Issue
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:
- https://salahuddin-s.medium.com/custom-header-based-authentication-using-spring-security-17f4163d0986
- https://www.baeldung.com/spring-security-granted-authority-vs-role
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);
}
@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();
}
}
Answered By - superflux
Answer Checked By - Dawn Plyler (JavaFixing Volunteer)