Shared SecurityContextHolder across multiple WebSecurityConfigurerAdapters - spring

Context: I'm writing a web app that is meant to serve two different API's, each with their own authentication filter. Both filters process and authenticate JWT tokens, however the tokens themselves contain different payloads and come from different authentication sources. Also, even though the tokens are different, and come from different sources, they share the same credentials for logging in, and are secured using the same key.
Ie: You could go to either one of these auth urls, with the same credentials and get back a different JWT token, with entirely different payloads.
/auth/auth1/login
/auth/auth2/login
Problem: The problem I am encountering is that if I authenticate to one of them, I have access to the other. Even without providing a token.
Meaning if I go to /requests/something, authenticate using a bearer token from auth provider 1, then go to /ims/oneroster/v1p1/somethingElse (without passing in a different token from auth provider 2) I am able to access data, even though I haven't authenticated using the filter associated with that path.
At the moment, the only way I know of to make sure that each filter is correctly checking if the user's token is valid is to put SecurityContextHolder.clearContext(); at the top of each of the filters doFilterInternal method. However, I'm pretty sure I shouldn't be doing that.
Can anyone see a problem with what I have below, or make some recommendations?
SecurityConfig.class
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
//https://docs.spring.io/spring-security/site/docs/current/reference/html/jc.html#multiple-httpsecurity
#Configuration #Order(1)
public static class XPressWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
private final CacheService cacheService;
public XPressWebSecurityConfigurationAdapter(CacheService cacheService) {this.cacheService = cacheService;}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/requests/**")
.authorizeRequests().anyRequest().authenticated()
.and()
.addFilter(new JWTAuthorizationFilter(authenticationManagerBean(), cacheService))
//.exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint())
;
}
}
#Configuration #Order(2)
public static class OneRosterWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
private final CacheService cacheService;
public OneRosterWebSecurityConfigurationAdapter(CacheService cacheService) {this.cacheService = cacheService;}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/ims/oneroster/v1p1/**")
.authorizeRequests().anyRequest().authenticated()
.and().addFilter(new OneRosterAuthorizationFilter(authenticationManagerBean(), cacheService))
;
}
}
}
OneRosterAuthorizationFilter.class
public class OneRosterAuthorizationFilter extends BasicAuthenticationFilter {
private final CacheService cacheService;
public OneRosterAuthorizationFilter(AuthenticationManager authManager, CacheService cacheService) {
super(authManager);
this.cacheService = cacheService;
}
#Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
logger.debug("GOING TO ONEROSTER FILTER");
AuthRequest authRequest = new AuthRequest(req);
if(authRequest.isAuthEnabled()) {
if(authRequest.isHeader() || (authRequest.isParameter() && authRequest.isAllowTokenParameter())) {
UsernamePasswordAuthenticationToken authentication = getAuthentication(req, authRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest req, AuthRequest authRequest) {
if(StringUtils.isBlank(authRequest.getToken())) {
return null; //Token was blank... 403 Forbidden
}
DecodedToken decodedToken = TokenDecoder.decodeToken(authRequest.getToken());
Application application = null;
if(decodedToken != null) {
application = new Application(decodedToken.getAppId(), decodedToken.getToken(), cacheService);
}
try {
if(!System.getenv("provider_id").equalsIgnoreCase(decodedToken.getProviderId())) {
throw new JWTVerificationException("Provider Ids Don't Match....");
}
if(application != null && StringUtils.isNotBlank(application.getApp().getProviderSecret())) {
JWT.require(Algorithm.HMAC256(application.getApp().getProviderSecret().getBytes()))
.withIssuer(PropertiesLoader.getInstance().getProperty("security.auth.jwt.issuer"))
.build().verify(authRequest.getToken());
return new UsernamePasswordAuthenticationToken(application, decodedToken.getToken(), getACLs(application));
}
}
catch (JWTVerificationException exception) {
//https://medium.com/fullstackblog/spring-security-jwt-token-expired-custom-response-b85437914b81
req.setAttribute("JWTVerificationException", exception.getMessage());
return null;
}
return null; //DecodedToken or Application was null... 403 Forbidden
}
private Collection<GrantedAuthority> getACLs(Application application) {
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
application.getPermissions().forEach(pathPermission -> {
if(pathPermission.getGet()) {
grantedAuthorities.add(new SimpleGrantedAuthority("get:" + pathPermission.getPath()));
}
if(pathPermission.getPost()) {
grantedAuthorities.add(new SimpleGrantedAuthority("post:" + pathPermission.getPath()));
}
if(pathPermission.getPut()) {
grantedAuthorities.add(new SimpleGrantedAuthority("put:" + pathPermission.getPath()));
}
if(pathPermission.getDelete()) {
grantedAuthorities.add(new SimpleGrantedAuthority("delete:" + pathPermission.getPath()));
}
});
return grantedAuthorities;
}
}
JWTAuthorizationFilter.class
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
private final CacheService cacheService;
public JWTAuthorizationFilter(AuthenticationManager authManager, CacheService cacheService) {
super(authManager);
this.cacheService = cacheService;
}
#Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
logger.debug("GOING TO JWT FILTER");
AuthRequest authRequest = new AuthRequest(req);
if(authRequest.isAuthEnabled()) {
if(authRequest.isHeader() || (authRequest.isParameter() && authRequest.isAllowTokenParameter())) {
UsernamePasswordAuthenticationToken authentication = getAuthentication(req, authRequest);
if(authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest req, AuthRequest authRequest) {
if(StringUtils.isBlank(authRequest.getToken())) {
return null; //Token was blank... 403 Forbidden
}
DecodedToken decodedToken = TokenDecoder.decodeToken(authRequest.getToken());
Application application = null;
if(decodedToken != null) {
application = new Application(decodedToken.getApplication_id(), decodedToken.getToken(), cacheService);
}
try {
if(application != null && StringUtils.isNotBlank(application.getApp().getProviderSecret())) {
JWT.require(Algorithm.HMAC256(application.getApp().getProviderSecret().getBytes()))
.withIssuer(PropertiesLoader.getInstance().getProperty("security.auth.jwt.issuer"))
.build().verify(authRequest.getToken());
return new UsernamePasswordAuthenticationToken(application, decodedToken.getToken(), getACLs(application));
}
}
catch (JWTVerificationException exception) {
//https://medium.com/fullstackblog/spring-security-jwt-token-expired-custom-response-b85437914b81
req.setAttribute("JWTVerificationException", exception.getMessage());
return null;
}
return null; //DecodedToken or Application was null... 403 Forbidden
}
private Collection<GrantedAuthority> getACLs(Application application) {
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
application.getPermissions().forEach(pathPermission -> {
if(pathPermission.getGet()) {
grantedAuthorities.add(new SimpleGrantedAuthority("get:" + pathPermission.getPath()));
}
if(pathPermission.getPost()) {
grantedAuthorities.add(new SimpleGrantedAuthority("post:" + pathPermission.getPath()));
}
if(pathPermission.getPut()) {
grantedAuthorities.add(new SimpleGrantedAuthority("put:" + pathPermission.getPath()));
}
if(pathPermission.getDelete()) {
grantedAuthorities.add(new SimpleGrantedAuthority("delete:" + pathPermission.getPath()));
}
});
return grantedAuthorities;
}
}
Note: Application is the class that implements UserDetails

After reading a comment from dur, here is the resolution to my problem. All I needed to do was add:
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
to each of the HttpSecurity objects.
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
//https://docs.spring.io/spring-security/site/docs/current/reference/html/jc.html#multiple-httpsecurity
#Configuration #Order(1)
public static class XPressWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
private final CacheService cacheService;
public XPressWebSecurityConfigurationAdapter(CacheService cacheService) {this.cacheService = cacheService;}
#Override
protected void configure(HttpSecurity http) throws Exception {
//https://auth0.com/blog/implementing-jwt-authentication-on-spring-boot/
http.antMatcher("/requests/**")
.authorizeRequests().anyRequest().authenticated()
.and().addFilter(new JWTAuthorizationFilter(authenticationManagerBean(), cacheService))
.exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint())
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
#Configuration #Order(2)
public static class OneRosterWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
private final CacheService cacheService;
public OneRosterWebSecurityConfigurationAdapter(CacheService cacheService) {this.cacheService = cacheService;}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/ims/oneroster/v1p1/**")
.authorizeRequests().anyRequest().authenticated()
.and().addFilter(new OneRosterAuthorizationFilter(authenticationManagerBean(), cacheService))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
}

Related

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:
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.
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();
}
}

Add additional user requirements in spring security login and handle various exceptions

I am new to Spring security, I have implemented a basic user login functionality for my app using JWT. Aside from checking for username and password at login I would like to add other parameters such as a "account is verified" boolean condition but I am not sure where to add this requirement. Additionally, I need to return a 403 forbidden response status message if the "account is verified" condition is false and return a different response status message if the username password combination isn't found at all. Here Is the code I currently have which correctly handles the login of an existing user (without checking for the "account is verified" condition) and always throws a 401 when the user is found. Any feedback would be helpful.
WebSecurityConfigurerAdapter
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final ApplicationUserDetailsService applicationUserDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurityConfig(ApplicationUserDetailsService userDetailsService) {
this.applicationUserDetailsService = userDetailsService;
this.bCryptPasswordEncoder = new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors()
.and()
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.addFilter(new AuthenticationFilter(authenticationManager()))
.addFilter(new AuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
public PasswordEncoder encoder() {
return this.bCryptPasswordEncoder;
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(applicationUserDetailsService)
.passwordEncoder(bCryptPasswordEncoder);
}
}
UserDetailsService
public class ApplicationUserDetailsService implements UserDetailsService {
private final ApplicationUserRepository applicationUserRepository;
public ApplicationUserDetailsService(ApplicationUserRepository applicationUserRepository) {
this.applicationUserRepository = applicationUserRepository;
}
#Override
public UserDetails loadUserByUsername(String nickname)
throws UsernameNotFoundException, UserIsNotActiveException {
Optional<ApplicationUser> applicationUser =
applicationUserRepository.findByNickname(nickname);
if (!applicationUser.isPresent()) {
throw new UsernameNotFoundException(nickname);
}
return new User(
applicationUser.get().getNickname(),
applicationUser.get().getPassword(),
emptyList());
}
}
AuthenticationFilter
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
ApplicationUser applicationUser =
new ObjectMapper().readValue(req.getInputStream(), ApplicationUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
applicationUser.getNickname(),
applicationUser.getPassword(),
new ArrayList<>()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(
HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) {
Date exp = new Date(System.currentTimeMillis() + EXPIRATION_TIME);
Key key = Keys.hmacShaKeyFor(KEY.getBytes());
Claims claims = Jwts.claims().setSubject(((User) auth.getPrincipal()).getUsername());
String token =
Jwts.builder()
.setClaims(claims)
.signWith(key, SignatureAlgorithm.HS512)
.setExpiration(exp)
.compact();
res.addHeader("token", token);
}
}
AuthorizationFilter
public AuthorizationFilter(AuthenticationManager authManager) {
super(authManager);
}
#Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader(HEADER_NAME);
if (header == null) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = authenticate(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken authenticate(HttpServletRequest request) {
String token = request.getHeader(HEADER_NAME);
if (token != null) {
Jws<Claims> user =
Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(KEY.getBytes()))
.build()
.parseClaimsJws(token);
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
} else {
return null;
}
}
return null;
}
ApplicationUser
public class ApplicationUser {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(unique = true)
String email;
#Column(unique = true)
String nickname;
String biography;
String password; // Hashed
#Builder.Default boolean isActive = false;
}
The interface UserDetails (that is returned by the UserDetailsService) has some utility methods that can help you with it.
While the account is not activated, you can return false from the UserDetails#isEnabled method, or maybe you can use UserDetails#isAccountNonLocked as well.
Those methods will then be automatically validated on the AbstractUserDetailsAuthenticationProvider$Default(Pre/Post)AuthenticationChecks class.
After the user goes through the activation flow, you can change the property to true and it will allow the user to authenticate.
Tip: add the logging.level.org.springframework.security=TRACE to your application.properties to help to debug.

Authenticate all APIs using Spring Security - HMAC based Custom Authentication

I am trying to implement HMAC Based authentication for all the REST API for my Microservice application (To know more about HMAC refer this). I have implemented below Spring security code but unable to get the app working with Spring security.
RESTSecurityConfig
#Configuration
#EnableWebSecurity
#Order(1)
public class RESTSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public RESTSecurityFilter authenticationFilter() throws Exception {
RESTSecurityFilter authenticationFilter = new RESTSecurityFilter("/");
authenticationFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationFilter;
}
#Bean
public RESTAuthenticationProvider authenticationProvider() {
RESTAuthenticationProvider provider = new RESTAuthenticationProvider();
return provider;
}
#Autowired
public void configAuthentication(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().addFilterBefore(authenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
}
}
RESTSecurityFilter
public class RESTSecurityFilter extends AbstractAuthenticationProcessingFilter {
private static final Logger log = LoggerFactory.getLogger(RESTSecurityFilter.class);
private static final String ACCESS_KEY_PARAMETER_NAME = "x-access-key";
private static final String SIGNATURE_PARAMETER_NAME = "x-signature";
private static final String NONCE_PARAMETER_NAME = "x-nonce";
private static final String TIMESTAMP_PARAMETER_NAME = "x-timestamp";
private static final String SECRET_KEY = "xxxxxxxxxxxxxxxxx";
protected RESTSecurityFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String accessKey = getHeaderValue(request, ACCESS_KEY_PARAMETER_NAME);
String signature = getHeaderValue(request, SIGNATURE_PARAMETER_NAME);
String nonce = getHeaderValue(request, NONCE_PARAMETER_NAME);
String timestamp = getHeaderValue(request, TIMESTAMP_PARAMETER_NAME);
String message = accessKey + ":" + nonce + ":" + timestamp;
String hashSignature = null;
try {
hashSignature = HMacUtility.calculateHmac(message, SECRET_KEY);
log.info("hashSignature : {}", hashSignature);
}
catch (InvalidKeyException | SignatureException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
AbstractAuthenticationToken authRequest = createAuthenticationToken(accessKey,
new RESTCredentials(signature, hashSignature));
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
private String getHeaderValue(HttpServletRequest request, String headerParameterName) {
return (request.getHeader(headerParameterName) != null) ? request.getHeader(headerParameterName) : "";
}
private AbstractAuthenticationToken createAuthenticationToken(String apiKeyValue, RESTCredentials restCredentials) {
return new RESTAuthenticationToken(apiKeyValue, restCredentials);
}
protected void setDetails(HttpServletRequest request, AbstractAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
#Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return true;
}
}
RESTAuthenticationProvider
public class RESTAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private static final Logger log = LoggerFactory.getLogger(RESTAuthenticationProvider.class);
#Autowired
private UserSecurityService userSecurityService;
#Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
RESTAuthenticationToken token = (RESTAuthenticationToken) authentication;
if (token != null) {
if (authentication.getCredentials() == null) {
log.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage("badCredentials", "Bad credentials"));
}
RESTCredentials restCredentials = (RESTCredentials) authentication.getCredentials();
log.info("==========userDetails.getPassword() = {}", userDetails.getPassword());
log.info("=============restCredentials.getRequestSalt() = {}", restCredentials.getRequestSalt());
log.info("=============restCredentials.getSecureHash() = {}", restCredentials.getSecureHash());
// Check if signature and hashSignature matches and return API response
} else {
throw new AuthenticationCredentialsNotFoundException(
MessageFormat.format("Expected Authentication Token object of type {0}, but instead received {1}",
RESTAuthenticationToken.class.getSimpleName(), authentication.getClass().getSimpleName()));
}
}
#Override
protected UserDetails retrieveUser(String apiKey, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
log.info("Loading user by apikey = {}", apiKey);
UserDetails loadedUser;
try {
loadedUser = userSecurityService.getUserByApiKey(apiKey);
log.info("########### Loaded user = {}", loadedUser);
} catch (UsernameNotFoundException notFound) {
throw notFound;
}
if (loadedUser == null) {
throw new AuthenticationServiceException("UserSecurityServiceImpl returned null, which is an interface contract violation");
}
return loadedUser;
}
}
RESTAuthenticationToken
public class RESTAuthenticationToken extends UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = 1L;
public RESTAuthenticationToken(Object principal, Object credentials) {
super(principal, credentials);
}
public RESTAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
UserSecurityRepository
public interface UserSecurityRepository {
UserDetails getUserByUsername(String username);
UserDetails getUserByApiKey(String apiKey);
}
UserSecurityService
#Service
public interface UserSecurityService extends UserDetailsService {
UserDetails getUserByApiKey(String apiKey);
}
Is there a simple and efficient way to Authenticate API in filter without the provider? I have user sent "signature" and "hashSignature" both in filter so just want to compare them and return the API json response if both of them match.
Any help is very much appreciated! Thanks

Getting null token in Jwt token filter when a request is sent in?

Just started using Jwt tokens when securing my microservices and keep getting a null token in my JwtTokenFilter class when a request is sent in but don't know where from, and finding it hard to understand why?
JwtTokenFilter.class
public class JwtTokenFilter extends OncePerRequestFilter {
private JwtTokenProvider jwtTokenProvider;
public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = jwtTokenProvider.resolveToken(request);
System.out.println("Token: " + token);
try {
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (CustomException ex) {
SecurityContextHolder.clearContext();
response.sendError(ex.getHttpStatus().value(), ex.getMessage());
return;
}
filterChain.doFilter(request, response);
}
SecurityConfig.class
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtTokenProvider jwtTokenProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth/login").permitAll()
.antMatchers("/auth/register").permitAll()
.antMatchers("/auth/{username}").permitAll()
.anyRequest()
.authenticated();
http.addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
}
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}
JwtTokenProvider.class
#Component
public class JwtTokenProvider {
#Value("$security.jwt.token.secret-key")
private String secretKey;
private long validityInMilliseconds = 3600000;
#Autowired
private CustomUserDetails customUserDetails;
#PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
public String createToken(String username, List<Role> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).collect(Collectors.toList()));
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()//
.setClaims(claims)//
.setIssuedAt(now)//
.setExpiration(validity)//
.signWith(SignatureAlgorithm.HS256, secretKey)//
.compact();
}
public Authentication getAuthentication(String token) {
UserDetails userDetails = customUserDetails.loadUserByUsername(getUsername(token));
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
public String getUsername(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
public String resolveToken(HttpServletRequest req) {
String bearerToken = req.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
throw new CustomException("Expired or invalid JWT token", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
If you need any more classes shown just ask, thanks for the help.
Realised the problem was how I registered it with my Gateway microservice in the spring configuration. All sorted thanks for the help.

Spring Session + REST + Custom Authentication Filter(read credentials from JSON rather query param)

I'm trying to convert my rest services authentication from basic authentication to form based authentication the below code works fine(Note I've commented out custom authentication filter) if I send authentication details in url as query parameter something like this http://localhost:8080/login?username=dfdf&&password=sdsdd
However I'm not keen on sending credentials as query parameter instead I would prefer to send it as json, Hence I've created custom authentication filter. When I add the custom authentication filter, my spring session stops working. I cannot find x-auth-token field in my response header. Any suggestion how to enable spring session and custom authentication together/or may be easier way to handle json input for credentials.
#Configuration
#EnableWebSecurity
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private ObjectMapper objectMapper;
#Bean
public PasswordEncoder passwordEncoder() {
PasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder;
}
#Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/register", "/index.html").permitAll().and().authorizeRequests()
.anyRequest().authenticated().and().requestCache().requestCache(new NullRequestCache()).and()
.formLogin().failureHandler(getRESTAuthenticationFailureHandler())
.successHandler(getRESTAuthenticationSuccessHandler()).usernameParameter("username")
.passwordParameter("password").and().exceptionHandling()
.authenticationEntryPoint(getRESTAuthenticationEntryPoint()).and()
//.addFilter(getAuthenticationFilter())
.csrf().disable();
}
#Bean
public HttpSessionStrategy httpSessionStrategy() {
return new HeaderHttpSessionStrategy();
}
#Bean
public RESTAuthenticationEntryPoint getRESTAuthenticationEntryPoint() {
return new RESTAuthenticationEntryPoint();
}
#Bean
public RESTAuthenticationSuccessHandler getRESTAuthenticationSuccessHandler() {
return new RESTAuthenticationSuccessHandler();
}
#Bean
public RESTAuthenticationFailureHandler getRESTAuthenticationFailureHandler() {
return new RESTAuthenticationFailureHandler();
}
#Bean
public AuthenticationFilter getAuthenticationFilter() {
AuthenticationFilter filter = new AuthenticationFilter();
try {
filter.setAuthenticationManager(this.authenticationManager());
} catch (Exception e) {
e.printStackTrace();
}
return filter;
}
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
public class RESTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
clearAuthenticationAttributes(request);
}
}
public class RESTAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
super.onAuthenticationFailure(request, response, exception);
}
}
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final Logger LOG = LoggerFactory.getLogger(AuthenticationFilter.class);
private boolean postOnly = true;
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = null;
String password = null;
UserDetails userDetails = null;
if ("application/json".equals(request.getHeader("Content-Type"))) {
userDetails = getJson(request);
if (userDetails != null) {
username = userDetails.getUsername();
}
} else {
username = obtainUsername(request);
}
if ("application/json".equals(request.getHeader("Content-Type"))) {
if (userDetails != null) {
password = userDetails.getPassword();
}
} else {
password = obtainPassword(request);
}
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private UserDetails getJson(HttpServletRequest request) {
try {
final List<String> data = IOUtils.readLines(request.getReader());
final String jsonData = data.stream().collect(Collectors.joining());
LOG.info(jsonData);
UserDetails userDetails = objectMapper.readValue(jsonData, UserDetails.class);
return userDetails;
} catch (IOException e) {
LOG.error("Failed to read data {}", e.getMessage(), e);
return null;
}
}
}
}
As suggested I've created custom filter which converts json object to request parameter and adds to HttpServletRequest, However are there any inbuilt spring security custom filters to do the same job?
#Configuration
#EnableWebSecurity
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private ObjectMapper objectMapper;
#Bean
public FilterRegistrationBean contextFilterRegistrationBean() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
CstFilter contextFilter = new CstFilter();
registrationBean.addUrlPatterns("/login");
registrationBean.setFilter(contextFilter);
registrationBean.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
return registrationBean;
}
#Bean
public PasswordEncoder passwordEncoder() {
PasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder;
}
#Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(getRESTAuthenticationEntryPoint())
.and()
.formLogin()
.permitAll()
.loginProcessingUrl("/login")
.failureHandler(getRESTAuthenticationFailureHandler())
.successHandler(getRESTAuthenticationSuccessHandler())
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout()
.permitAll()
.logoutSuccessHandler(getRESTLogoutSuccessHandler())
.and()
.authorizeRequests()
.antMatchers("/", "/index.html")
.permitAll()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.requestCache()
.requestCache(new NullRequestCache());
}
#Bean
public HttpSessionStrategy httpSessionStrategy() {
return new HeaderHttpSessionStrategy();
}
#Bean
public RESTAuthenticationEntryPoint getRESTAuthenticationEntryPoint() {
return new RESTAuthenticationEntryPoint();
}
#Bean
public RESTAuthenticationSuccessHandler getRESTAuthenticationSuccessHandler() {
return new RESTAuthenticationSuccessHandler();
}
#Bean
public RESTAuthenticationFailureHandler getRESTAuthenticationFailureHandler() {
return new RESTAuthenticationFailureHandler();
}
#Bean
public RESTLogoutSuccessHandler getRESTLogoutSuccessHandler() {
return new RESTLogoutSuccessHandler();
}
public class JsonConvertFilter extends HttpServletRequestWrapper {
private final Logger LOG = LoggerFactory.getLogger(JsonConvertFilter.class);
private UserDetails userDetails;
public JsonConvertFilter(HttpServletRequest request) {
super((HttpServletRequest)request);
userDetails = getJson();
}
public String getParameter(String key){
if(userDetails!=null){
if("username".equals(key)){
return userDetails.getUsername();
}
if("password".equals(key)){
return userDetails.getPassword();
}
}
System.out.println("Called wrapper");
return super.getParameter(key);
}
private UserDetails getJson() {
try {
final List<String> data = IOUtils.readLines(super.getReader());
final String jsonData = data.stream().collect(Collectors.joining());
LOG.info(jsonData);
UserDetails userDetails = objectMapper.readValue(jsonData, UserDetails.class);
return userDetails;
} catch (IOException e) {
LOG.warn("Failed to read data {}", e.getMessage(), e);
return null;
}
}
}
public class CstFilter implements Filter{
#Override
public void destroy() {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(new JsonConvertFilter((HttpServletRequest)request), response);
}
#Override
public void init(FilterConfig arg0) throws ServletException {
}
}
public class RESTLogoutSuccessHandler implements LogoutSuccessHandler{
#Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String uri = request.getRequestURI();
if ("logout".equals(uri)) {
response.sendError(HttpServletResponse.SC_OK, "Succesfully Logged out");
}
}
}
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().print("Unauthorizated....");
}
}
public class RESTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
clearAuthenticationAttributes(request);
}
}
public class RESTAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String uri = request.getRequestURI();
if ("logout".equals(uri)) {
response.sendError(HttpServletResponse.SC_OK, "Succesfully Logged out");
} else {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"Authentication Failed: " + exception.getMessage());
}
}
}
}
For any one want to test this using jquery ajax.
/* Alerts the results */
$.ajax({
url : "login",
type : "POST",
async : false,
contentType: "application/json",
data : "{ \"username\":\""+username+"\", \"password\":\"" + password + "\"}",
success : function(data, status, request) {
alert("Success");
authtokenKey = request.getResponseHeader('x-auth-token');
},
error : function(xhr, status, error) {
alert(error);
}
});

Resources