Search code examples
springspring-bootspring-security

Spring Security For Authorities Based Filtering Using Custom Http Header


I am trying to implement RBAC using Spring Security. User authentication is implemented separately and sessionId is generated for the app to use. I wanted to have Spring Security take the sessionId from the Http Header and would use the sessionId to get the Authorities from a database to determine whether the user is authorized to access certain endpoints. The problem is that I don't know how to get the authorities from the database on demand and I don't know if the configuration is being done correctly. This is what I have so far:

@Configuration
@EnableWebSecurity
public class CustomSecurityFilter {

  @Bean
  AuthenticationManager customAuthenticationManager(HttpHeaderAuthenticationProvider httpHeaderAuthenticationProvider) {
    return new ProviderManager(List.of(httpHeaderAuthenticationProvider));
  }

  @Bean
  HttpHeaderAuthenticationProvider newHttpHeaderAuthenticationProvider() {
    return new HttpHeaderAuthenticationProvider();
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http,
      AuthenticationManager authenticationManager) throws Exception {
    http.addFilterBefore(getFilter(authenticationManager), AnonymousAuthenticationFilter.class).authorizeRequests()
        .antMatchers(HttpMethod.GET, "/api/apples").hasAuthority("viewApples")
        .antMatchers(HttpMethod.POST, "/api/apples").hasAuthority("createApples")
    return http.build();
  }

  private Filter getFilter(AuthenticationManager authenticationManager) {
    return new HttpHeaderProcessingFilter(
        new OrRequestMatcher(
            new AntPathRequestMatcher("/api/apples/**"),
        ),
        authenticationManager
    );
  }
}
public class HttpHeaderAuthenticationProvider implements AuthenticationProvider {
  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    var sessionId = ((String) authentication.getPrincipal());

    // Somehow connect to database to get session and authorities information?
    boolean isValid = sessionId != null;
    if (isValid) {
      return newPreAuthenticatedToken("sessionId", List.of());
    } else {
      throw new AccessDeniedException("Invalid sessionId");
    }
  }

  @Override
  public boolean supports(Class<?> authentication) {
    return PreAuthenticatedAuthenticationToken.class.equals(authentication);
  }

  public static PreAuthenticatedAuthenticationToken newPreAuthenticatedToken(String userId, List<String> permissions) {
    var grantedAuthorityList = new ArrayList<GrantedAuthority>();
    for (String permission : permissions) {
      grantedAuthorityList.add(new SimpleGrantedAuthority(permission));
    }

    return new PreAuthenticatedAuthenticationToken(userId, null, grantedAuthorityList);
  }
}
public class HttpHeaderProcessingFilter extends AbstractAuthenticationProcessingFilter {

  public HttpHeaderProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher,
                                    AuthenticationManager authenticationManager) {
    super(requiresAuthenticationRequestMatcher);
    setAuthenticationManager(authenticationManager);
  }

  @Override
  public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
      throws AuthenticationException {
    return getAuthenticationManager().authenticate(
        // Not sure if we are supposed to do this
        HttpHeaderAuthenticationProvider.newPreAuthenticatedToken("sessionId", List.of())
    );
  }

  @Override
  protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                          Authentication authResult) throws IOException, ServletException {
    SecurityContextHolder.getContext().setAuthentication(authResult);
    chain.doFilter(request, response);
  }
}

I tried using these resources:

I was also wondering whether JWT would be a good candidate to use in place of a custom sessionId with RBAC + Session Handling.


Solution

  • I was able to configure the filter to use authorities. Here is what I have:

    @Component
    @Slf4j
    public class CustomPreAuthProvider implements AuthenticationProvider {
    
      private boolean throwExceptionWhenTokenRejected;
    
      @Override
      public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if (!this.supports(authentication.getClass())) {
          return null;
        }  else {
          log.debug(String.valueOf(LogMessage.format("PreAuthenticated authentication request: %s", authentication)));
          if (authentication.getPrincipal() == null) {
            log.debug("No pre-authenticated principal found in request.");
            if (this.throwExceptionWhenTokenRejected) {
              throw new BadCredentialsException("No pre-authenticated principal found in request.");
            } else {
              return null;
            }
          } else if (authentication.getCredentials() == null) {
            log.debug("No pre-authenticated credentials found in request.");
            if (this.throwExceptionWhenTokenRejected) {
              throw new BadCredentialsException("No pre-authenticated credentials found in request.");
            } else {
              return null;
            }
          } else if (!authentication.isAuthenticated()) {
            throw new InsufficientAuthenticationException("Access token likely no longer valid.");
          }
    
          return authentication;
        }
      }
    
      @Override
      public boolean supports(Class<?> authentication) {
        return authentication.equals(PreAuthenticatedAuthenticationToken.class);
      }
    
      public void setThrowExceptionWhenTokenRejected(boolean throwExceptionWhenTokenRejected) {
        this.throwExceptionWhenTokenRejected = throwExceptionWhenTokenRejected;
      }
    }
    
    @Service
    public class CustomUserDetails implements UserDetailsService {
      @Autowired
      private SessionRepository sessionRepository;
    
      @Autowired
      private RoleRepository roleRepository;
    
      @Autowired
      private AuthHelper authHelper;
    
      @Override
      public UserDetails loadUserByUsername(String sessionId) throws UsernameNotFoundException, IllegalStateException {
        var sessions = sessionRepository.getSession(sessionId);  // Database query for session information
        if (sessions == null || sessions.isEmpty()) {
          throw new UsernameNotFoundException("Session Not Found");
        } else if (sessions.size() > 1) {
          throw new IllegalStateException("More than one record with sessionId found");
        }
    
        var session = sessions.get(0);
    
        var authoritySet = new HashSet<String>();
        for (String role : session.getRoles()) {
          var authorities = roleRepository.getUserPrivilegesByRoleName(role);  // Database query for authorities
          for (UserRolePrivilege userRolePrivilege : authorities) {
            authoritySet.add(userRolePrivilege.getPermittedAction());
          }
        }
    
        var grantedAuthority = new ArrayList<GrantedAuthority>();
        for (String authority : authoritySet) {
          grantedAuthority.add(new SimpleGrantedAuthority(authority));
        }
    
        var introspect = authHelper.validateAccessToken(session.getSessionId(), session.getAccessToken(),
            session.getRefreshToken(), session.getExpirationTime());  // Code to verify token
    
        var user = new UserImpl();
        user.setUsername(session.getEmail());
        user.setPassword(session.getAccessToken());
        user.setEnabled(introspect.getIntrospect().isActive());
        user.setAccountNonExpired(introspect.getIntrospect().isActive());
        user.setAccountNonLocked(introspect.getIntrospect().isActive());
        user.setCredentialsNonExpired(introspect.getIntrospect().isActive());
        user.setAuthorities(grantedAuthority);
    
        return user;
      }
    }
    
    public class SessionAuthFilter extends AbstractAuthenticationProcessingFilter {
      private final CustomUserDetails customUserDetails;
    
      protected SessionAuthFilter(RequestMatcher requestMatcher, AuthenticationManager authenticationManager,
          CustomUserDetails customUserDetails) {
        super(requestMatcher, authenticationManager);
        this.customUserDetails = customUserDetails;
        this.setContinueChainBeforeSuccessfulAuthentication(true);
        this.setAuthenticationSuccessHandler((request, response, authentication) -> {});
      }
    
      @Override
      public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
          throws AuthenticationException {
        var sessionId = request.getHeader("sessionId") != null ? request.getHeader("sessionId").trim() : null;
        var user = customUserDetails.loadUserByUsername(sessionId);
        var authentication = new PreAuthenticatedAuthenticationToken(user.getUsername(), user.getPassword(),
            user.getAuthorities());
        authentication.setAuthenticated(user.isCredentialsNonExpired());
        authentication.setDetails(customUserDetails);
    
        SecurityContextHolder.getContext().setAuthentication(authentication);
        return this.getAuthenticationManager().authenticate(authentication);
      }
    }
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
    
      @Bean
      AuthenticationManager customAuthenticationManager(CustomPreAuthProvider preAuthProvider) {
        return new ProviderManager(List.of(preAuthProvider));
      }
    
      @Bean
      SessionAuthFilter customAuthFilter(AuthenticationManager authManager, CustomUserDetails customUserDetails) {
        return new SessionAuthFilter(
            new OrRequestMatcher(
                new AntPathRequestMatcher("/apples/**"),
            ),
            authManager,
            customUserDetails);
      }
    
      @Bean
      public SecurityFilterChain filterChain(HttpSecurity http, SessionAuthFilter authFilter) throws Exception {
        http.exceptionHandling()
            .authenticationEntryPoint(new Http403ForbiddenEntryPoint())
            .accessDeniedHandler(new AccessDeniedHandlerImpl())
            .and()
            .formLogin().disable()
            .httpBasic().disable()
            .authorizeRequests()
            .antMatchers(
                "/",
                "/error",
                "/v3/api-docs/**",
                "/swagger-ui/**",
                "/swagger-ui.html",
                "/actuator/**"
            ).permitAll()
            .antMatchers(HttpMethod.GET, "/apples").hasAuthority("viewApples")
            .antMatchers(HttpMethod.POST, "/apples").hasAuthority("createApples")
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(authFilter, AbstractPreAuthenticatedProcessingFilter.class)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
        return http.build();
      }
    }