Handling OPTIONS and CORS when using a sign in filter instead of controller - spring

I've got an AbstractAuthenticationProcessingFilter that I'm using to handle POST requests at path /sign-in. CORS preflight requests are coming back 404 because there is no path that matches. This makes sense to me.
What I would like to know is if there is a way to inform Spring that there is a filter handling the POST (rather than a controller), so that Spring can dispatch the OPTIONS in the same way it would if a controller were handling the POST. Would it be bad practice to write a controller with one PostMapping? I'm not sure how that would behave since technically the filter handles the POST.
Thanks for your help!
Update
Here's my setup. I originally posted from my phone so wasn't able to add these details then. See below. To reiterate, there is no controller for /sign-in. The POST is handled by the JwtSignInFilter.
CORS Config
#EnableWebMvc
#Configuration
public class CorsConfig extends WebMvcConfigurerAdapter {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*") // TODO: Lock this down before deploying
.allowedHeaders("*")
.allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())
.allowCredentials(true);
}
}
Security Config
#EnableWebSecurity
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public JwtSignInFilter signInFilter() throws Exception {
return new JwtSignInFilter(
new AntPathRequestMatcher("/sign-in", HttpMethod.POST.name()),
authenticationManager()
);
}
#Bean
public JwtAuthenticationFilter authFilter() {
return new JwtAuthenticationFilter();
}
#Autowired
private UserDetailsService userDetailsService;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers(HttpMethod.POST, "/sign-in").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(
signInFilter(),
UsernamePasswordAuthenticationFilter.class
)
.addFilterBefore(
authFilter(),
UsernamePasswordAuthenticationFilter.class
);
}
}
Sign In Filter
public class JwtSignInFilter extends AbstractAuthenticationProcessingFilter {
#Autowired
private TokenAuthenticationService tokenAuthService;
public JwtSignInFilter(RequestMatcher requestMatcher, AuthenticationManager authManager) {
super(requestMatcher);
setAuthenticationManager(authManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException {
SignInRequest creds = new ObjectMapper().readValue(
req.getInputStream(),
SignInRequest.class
);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getEmail(),
creds.getPassword(),
emptyList()
)
);
}
#Override
protected void successfulAuthentication(
HttpServletRequest req,
HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
tokenAuthService.addAuthentication(res, auth.getName());
}
}
Authentication Filter
public class JwtAuthenticationFilter extends GenericFilterBean {
#Autowired
private TokenAuthenticationService tokenAuthService;
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
Authentication authentication = tokenAuthService.getAuthentication((HttpServletRequest)request);
SecurityContextHolder
.getContext()
.setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}

Alright, finally found out how to fix this. After hours of tinkering and searching, I found that I needed to use a filter-based CORS configuration and then handle CORS preflights (OPTIONS requests) in the sign-in filter by simply returning 200 OK. The CORS filter will then add appropriate headers.
Updated configuration below (note that my CorsConfig is no longer needed, since we have a CORS filter in SecurityConfig, and JwtAuthenticationFilter is the same as before).
Security Config
#EnableWebSecurity
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*"); // TODO: lock down before deploying
config.addAllowedHeader("*");
config.addExposedHeader(HttpHeaders.AUTHORIZATION);
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Bean
public JwtSignInFilter signInFilter() throws Exception {
return new JwtSignInFilter(
new AntPathRequestMatcher("/sign-in"),
authenticationManager()
);
}
#Bean
public JwtAuthenticationFilter authFilter() {
return new JwtAuthenticationFilter();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/sign-in").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(
signInFilter(),
UsernamePasswordAuthenticationFilter.class
)
.addFilterBefore(
authFilter(),
UsernamePasswordAuthenticationFilter.class
);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Autowired
private UserDetailsService userDetailsService;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Sign In Filter
public class JwtSignInFilter extends AbstractAuthenticationProcessingFilter {
#Autowired
private TokenAuthenticationService tokenAuthService;
public JwtSignInFilter(RequestMatcher requestMatcher, AuthenticationManager authManager) {
super(requestMatcher);
setAuthenticationManager(authManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException {
if (CorsUtils.isPreFlightRequest(req)) {
res.setStatus(HttpServletResponse.SC_OK);
return null;
}
if (!req.getMethod().equals(HttpMethod.POST.name())) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
return null;
}
SignInRequest creds = new ObjectMapper().readValue(
req.getInputStream(),
SignInRequest.class
);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getEmail(),
creds.getPassword(),
emptyList()
)
);
}
#Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {
tokenAuthService.addAuthentication(res, auth.getName());
}
}

Related

SpringBoot different auths (MS AD & JWT) to one Controller

I tried to implement small API Gateway for my Mobile App on Spring Boot.
In my architecture i uses MS Active Directory Server for auth staff of company and in future will sms verify code for clients company for sending JWT.
I'm not use layer DAO, UsersRepository and DB connect.
All HTTP requests sending via RestTemplate from Services layer to our inthernal CRM-system.
I implements LDAP AD auth is very simple HttpBasic configuration bellow:
#Configuration
#EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and().csrf()
.disable()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/v1/send/**").permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider = new
ActiveDirectoryLdapAuthenticationProvider("mydomain.com", "ldap://192.168.0.100:389/");
activeDirectoryLdapAuthenticationProvider.setConvertSubErrorCodesToExceptions(true);
activeDirectoryLdapAuthenticationProvider.setUseAuthenticationRequestCredentials(true);
activeDirectoryLdapAuthenticationProvider.setSearchFilter("(&(objectClass=user)(userPrincipalName={0})(memberOf=CN=mobileaccess,OU=User Groups,OU=DomainAccountsUsers,DC=MYDOMAIN,DC=COM))");
auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider);
auth.eraseCredentials(true);
}
}
I have two RestController V1 and V2 for example:
#RequestMapping("api/v1")
//get token for staff (AD user) HttpBasic auth
#PostMapping("auth/get/stafftoken")
public ResponseEntity<?> getToken() {
// some code...
HttpHeaders tokenHeaders = new HttpHeaders();
tokenHeaders.setBearerAuth(tokenAuthenticationService.getToken());
return new ResponseEntity<>(tokenHeaders, HttpStatus.OK);
}
//get JWT if code from sms == code in my CRM-system (for client) not auth - permitAll
#PostMapping("send/clienttoken")
public #ResponseStatus
ResponseEntity<?> sendVerifyCode(#RequestParam("verifycode") String verifycode) {
// some code...
HttpHeaders tokenHeaders = new HttpHeaders();
tokenHeaders.setBearerAuth(tokenAuthenticationService.getToken());
return new ResponseEntity<>(tokenHeaders, HttpStatus.OK);
}
#RequestMapping("api/v2")
#GetMapping("get/contract/{number:[0-9]{6}")
public Contract getContract(#PathVariable String number) {
return contractsService.getContract(number);
}
How to implements Bearer Auth requests to Controller APIv2 with JWT tokens (clients and staff)?
I think this is implemented through filter chain?
So guys
If you implements multi authentification as in my example, first of all create utility class for builds token and validation users JWT. This is standard code, for example:
public static String createUserToken(Authentication authentication) {
return Jwts.builder()
.setSubject(authentication.getName())
.claim(authentication.getAuthorities())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SIGN_KEY)
.compact();
}
public static Authentication getAuthentication(HttpServletRequest request) {
String token = extractJwt(request);
try {
if (token != null) {
Claims claims = Jwts.parser().setSigningKey(SIGN_KEY).parseClaimsJws(token).getBody();
String username = Jwts.parser().setSigningKey(SIGN_KEY).parseClaimsJws(token).getBody().getSubject();
return username != null ? new UsernamePasswordAuthenticationToken(username, "", Collections.EMPTY_LIST) : null;
}
} catch (ExpiredJwtException e) {
}
return null;
}
Аfter you should create two filters:
LoginAuthentificationFilter extends BasicAuthenticationFilter
JwtAuthenticationFilter extends GenericFilterBean
Code example below
public class LoginAuthentificationFilter extends BasicAuthenticationFilter {
public LoginAuthentificationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
super.doFilterInternal(request, response, chain);
}
}
public class JwtAuthenticationFilter extends GenericFilterBean {
private RequestMatcher requestMatcher;
public JwtAuthenticationFilter(String path) {
this.requestMatcher = new AntPathRequestMatcher(path);
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (!requiresAuthentication((HttpServletRequest) servletRequest)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
Authentication authentication = JwtUtils.getAuthentication((HttpServletRequest) servletRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(servletRequest, servletResponse);
}
private boolean requiresAuthentication(HttpServletRequest request) {
return requestMatcher.matches(request);
}
}
And at the end
Settings WebSecurityConfigurerAdapter
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/v1/noauth_endpoints").permitAll()
.anyRequest()
.authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterAt(jwtFilter(), BasicAuthenticationFilter.class)
.addFilter(loginFilter());
http.headers().cacheControl();
}
Beans
#Bean
public LoginAuthentificationFilter loginFilter() {
return new LoginAuthentificationFilter(authenticationManager());
}
#Bean
public JwtAuthenticationFilter jwtFilter() {
return new JwtAuthenticationFilter("/api/v2/**");
}
#Bean
#Override
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(activeDirectoryLdapAuthenticationProvider()));
}
#Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider("bzaimy.com", "ldap://192.168.0.100:389/");
provider.setSearchFilter("(&(objectClass=user)(userPrincipalName={0})(memberOf=CN=mobileaccess,OU=User Groups,OU=DomainAccountsUsers,DC=MYDOMAIN,DC=COM))");
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
return provider;
}

Spring security: My Authorization filter authorizes my request even tho the URL is permited

In my security configuration class i have permitted the request to the welcome url and any other url which follows the "welcome/**" format.
this is my securityconfiguration class:
#EnableGlobalMethodSecurity(prePostEnabled = true)
//#Configuration
#EnableWebSecurity
public class JwtSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
private final CustomerDetailsService customerDetailsService;
#Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
#Autowired
public JwtSecurityConfiguration(CustomerDetailsService customerDetailsService) {
this.customerDetailsService = customerDetailsService;
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(customerDetailsService)
.passwordEncoder(passwordEncoderBean());
}
#Bean
public PasswordEncoder passwordEncoderBean() {
return new BCryptPasswordEncoder();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("**/resources/static/**")
.and()
.ignoring()
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/index_assets/**"
);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/welcome/login").permitAll()
.antMatchers("/welcome").permitAll()
.antMatchers("/welcome/signup").permitAll()
.antMatchers("admin/rest/**").authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//http.addFilterBefore(new JWTAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(new JWTAuthorizationFilter(authenticationManager(),customerDetailsService),UsernamePasswordAuthenticationFilter.class);
// disable page caching
http
.headers()
.frameOptions().sameOrigin() // required to set for H2 else H2 Console will be blank.
.cacheControl();
//http.headers().cacheControl();
}
}
but I noticed that in my JWTAuthorizationFilter.class the doFilterInternal() method picks up this URL
public class JWTAuthorizationFilter extends OncePerRequestFilter {
private final CustomerDetailsService customerDetailsService;
#Autowired
DefaultCookieService defaultCookieService;
public JWTAuthorizationFilter(AuthenticationManager authenticationManager, CustomerDetailsService customerDetailsService) {
// super(authenticationManager);
this.customerDetailsService = customerDetailsService;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader(HEADER);
if(Objects.isNull(header) || !header.startsWith(TOKEN_PREFIX)){
return;
}
UsernamePasswordAuthenticationToken usernamePasswordAuth = getAuthenticationToken(request);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuth);
chain.doFilter(request,response);
}
private UsernamePasswordAuthenticationToken getAuthenticationToken(HttpServletRequest request){
String token = request.getHeader(HEADER);
if(Objects.isNull(token)) return null;
String username = Jwts.parser().setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKEN_PREFIX,""))
.getBody()
.getSubject();
UserDetails userDetails = customerDetailsService.loadUserByUsername(username);
return username != null ? new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()) : null;
}
}
What is the cause of this ?
Filter is suppose to pick up each and every request. It doesn't matter if that you have permitted or not in security configuration.
You have got two options:
If you don't want welcome/** to go through the filter you add it to web ignore
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("**/resources/static/**")
.and()
.ignoring()
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/index_assets/**",
"/welcome/**"
);
}
But note, it will skip all filters and you may not want that.
In doFilterInternal method skip it when you find welcome/** pattern.

Spring security using JWT, how to exclude certain endpoints like /login from being authenticated using JwtAuthenticationFilter?

I want to exclude /login url from being authenticated by spring security.
My configuration class looks like'
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests().antMatchers("/v1/pricing/login").permitAll()
.antMatchers("v1/pricing/**").authenticated().and()
.addFilterBefore(corsFilter,UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v1/pricing/login");
}
JwtAuthenticationFilter looks like
- commented the exception part, as it starts throwing exception in login also
#Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
#Autowired
JwtTokenProvider jwtTokenProvider;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
String[] userInfo = jwtTokenProvider.getUserDetailsFromJWT(jwt);
UserDetails userDetails = new UserPrincipal(Long.parseLong(userInfo[0]), userInfo[1], userInfo[2], null,
userInfo[3]);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, null);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (StringUtils.hasText(token)) {
return token;
} /*else {
throw new AuthenticationServiceException("Authorization header cannot be blank!");
}*/
return null;
}
}
Any request with /v1/pricing/login still goes to JWtAuthentication filter and fails.
JwtTokenAuthenticationProcessingFilter filter is configured to skip following endpoints: /api/auth/login and /api/auth/token. This is achieved with SkipPathRequestMatcher implementation of RequestMatcher.
public class SkipPathRequestMatcher implements RequestMatcher {
private OrRequestMatcher matchers;
private RequestMatcher processingMatcher;
public SkipPathRequestMatcher(List<String> pathsToSkip, String processingPath) {
Assert.notNull(pathsToSkip);
List<RequestMatcher> m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList());
matchers = new OrRequestMatcher(m);
processingMatcher = new AntPathRequestMatcher(processingPath);
}
#Override
public boolean matches(HttpServletRequest request) {
if (matchers.matches(request)) {
return false;
}
return processingMatcher.matches(request) ? true : false;
}
}
Then call :
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List<String> pathsToSkip = Arrays.asList(TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT);
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter
= new JwtTokenAuthenticationProcessingFilter(failureHandler, tokenExtractor, matcher);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(ajaxAuthenticationProvider);
auth.authenticationProvider(jwtAuthenticationProvider);
}
#Bean
protected BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // We don't need CSRF for JWT based authentication
.exceptionHandling()
.authenticationEntryPoint(this.authenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
.antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
.antMatchers("/console").permitAll() // H2 Console Dash-board - only for testing
.and()
.authorizeRequests()
.antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected API End-points
.and()
.addFilterBefore(buildAjaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
}
}

permitAll() requires Authentication

I'm having a go at developing a REST application with Spring and using JWT for authentication.
At the moment, what I'm trying to achieve is:
GET /api/subjects/* should be accessible to all users.
POST /api/subjects/* should only accessible to admin users.
The issue is that for both cases, the JWT filter gets invoked and I get an error response stating the JWT token is missing.
I've implemented my WebSecurityConfig as follows, including a JWT filter to replace the BasicAuthenticationFilter:
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
JWTAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
JWTAuthenticationProvider jwtAuthenticationProvider;
#Override
public void configure(WebSecurity web) throws Exception {
//web.ignoring().antMatchers(HttpMethod.GET,"/api/subjects/*");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/subjects/*").permitAll()
.antMatchers(HttpMethod.POST, "/api/subjects/*").hasRole(Profile.Role.ADMIN.toString())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterAt(authenticationTokenFilter(), BasicAuthenticationFilter.class)
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
}
public JWTAuthenticationFilter authenticationTokenFilter() {
return new JWTAuthenticationFilter(authenticationManager(), authenticationEntryPoint);
}
public ProviderManager authenticationManager() {
return new ProviderManager(new ArrayList<AuthenticationProvider>(Arrays.asList(jwtAuthenticationProvider)));
}
}
My implementation of JWTAuthenticationFilter is based on the implementation of BasicAuthenticationFilter:
public class JWTAuthenticationFilter extends OncePerRequestFilter {
private static final String JWT_TOKEN_START = "JWT ";
private AuthenticationManager authenticationManager;
private AuthenticationEntryPoint authenticationEntryPoint;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
Assert.notNull(authenticationManager, "Authentication Manager must not be null");
Assert.notNull(authenticationEntryPoint, "Authentication Entry point must not be null");
this.authenticationManager = authenticationManager;
this.authenticationEntryPoint = authenticationEntryPoint;
}
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
String header = httpServletRequest.getHeader("Authorization");
if (header == null || !header.startsWith(JWT_TOKEN_START)) {
throw new IllegalStateException("Header does not contain: \"Authorization\":\"JWT <token>\". Value: "+header);
}
try {
String jwt = header.substring(JWT_TOKEN_START.length()).trim().replace("<", "").replace(">", "");
JWTAuthenticationToken jwtAuthenticationToken = new JWTAuthenticationToken(jwt);
this.authenticationManager.authenticate(jwtAuthenticationToken);
filterChain.doFilter(httpServletRequest, httpServletResponse);
} catch (AuthenticationException auth) {
SecurityContextHolder.clearContext();
this.authenticationEntryPoint.commence(httpServletRequest, httpServletResponse, auth);
}
}
}
What is causing this issue?

Issue with Spring Security remember me token not being set on SecurityContextHolder

I am encountering an issue with my remember me configuration:
[nio-8080-exec-8] s.s.w.a.r.RememberMeAuthenticationFilter : SecurityContextHolder not populated with remember-me token, as it already contained: 'org.springframework.security.authentication.UsernamePasswordAuthenticationToken#73939efa: Principal: Member ...
Here is my Spring security configuration:
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private MemberUserDetailsService memberUserDetailsService;
#Autowired
private BCryptPasswordEncoder passwordEncoder;
#Autowired
private AccessDecisionManager accessDecisionManager;
#Autowired
private ApplicationEventPublisher eventPublisher;
#Override
protected void configure(HttpSecurity http) throws Exception {
//#formatter:off
http
.headers()
.cacheControl()
.and()
.and()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.and()
.rememberMe()
.tokenValiditySeconds(60*60*24*7)
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.and()
.formLogin()
.loginProcessingUrl("/api/signin")
.failureHandler(authenticationFailureHandler())
.successHandler(authenticationSuccessHandler())
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/api/signout"))
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
.authorizeRequests()
.accessDecisionManager(accessDecisionManager)
.antMatchers("/resources/**", "/**").permitAll()
.anyRequest().authenticated();
//#formatter:on
}
private LogoutSuccessHandler logoutSuccessHandler() {
return new LogoutSuccessHandler() {
#Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setStatus(HttpStatus.OK.value());
}
};
}
private AccessDeniedHandler accessDeniedHandler() {
return new AccessDeniedHandler() {
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// TODO: deal with InvalidCsrfTokenException
response.setStatus(HttpStatus.FORBIDDEN.value());
}
};
}
private AuthenticationFailureHandler authenticationFailureHandler() {
return new AuthenticationFailureHandler() {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
};
}
private AuthenticationSuccessHandler authenticationSuccessHandler() {
return new AuthenticationSuccessHandler() {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Member member = (Member) authentication.getPrincipal();
eventPublisher.publishEvent(new SigninApplicationEvent(member));
// TODO: overhaul below
response.addHeader("MEMBER_ROLE", member.getRole().name());
response.setStatus(HttpStatus.OK.value());
}
};
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(memberUserDetailsService).passwordEncoder(passwordEncoder);
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
}
and also:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class CoreSecurityConfiguration {
#Bean
public MemberUserDetailsService memberUserDetailsService() {
return new MemberUserDetailsService();
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder;
}
#Bean
public SessionRegistryImpl sessionRegistry() {
SessionRegistryImpl sessionRegistry = new SessionRegistryImpl();
return sessionRegistry;
}
#Bean
public AffirmativeBased accessDecisionManager() {
AffirmativeBased accessDecisionManager = new AffirmativeBased(accessDecisionVoters());
return accessDecisionManager;
}
private List<AccessDecisionVoter<? extends Object>> accessDecisionVoters() {
List<AccessDecisionVoter<? extends Object>> accessDecisionVoters = new ArrayList<>();
accessDecisionVoters.add(roleHierarchyVoter());
accessDecisionVoters.add(webExpressionVoter());
return accessDecisionVoters;
}
#Bean
public WebExpressionVoter webExpressionVoter() {
WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
webExpressionVoter.setExpressionHandler(defaultWebSecurityExpressionHandler());
return webExpressionVoter;
}
#Bean
public DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler() {
DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy());
return defaultWebSecurityExpressionHandler;
}
#Bean
public RoleHierarchyVoter roleHierarchyVoter() {
RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy());
return roleHierarchyVoter;
}
#Bean
public RoleHierarchyImpl roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
//#formatter:off
roleHierarchy.setHierarchy(
"ROLE_ADMINISTRATOR > ROLE_MODERATOR\n" +
"ROLE_MODERATOR > ROLE_SUBSCRIBED_PARENTS\n" +
"ROLE_MODERATOR > ROLE_SUBSCRIBED_CHILDCARE_WORKER\n" +
"ROLE_SUBSCRIBED_PARENTS > ROLE_BASIC_PARENTS\n" +
"ROLE_SUBSCRIBED_CHILDCARE_WORKER > ROLE_BASIC_CHILDCARE_WORKER");
//#formatter:on
return roleHierarchy;
}
}
Can somemone please help?
edit 1:
MemberUserDetailsService:
#Component
public class MemberUserDetailsService implements UserDetailsService {
#Autowired
private MemberRepository memberRepository;
#Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Member member = memberRepository.findByEmail(email);
if (member == null) {
throw new UsernameNotFoundException("Username: " + email + " not found!");
}
return member;
}
}
edit 2: Here is the new config:
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private MemberUserDetailsService memberUserDetailsService;
#Autowired
private BCryptPasswordEncoder passwordEncoder;
#Autowired
private AccessDecisionManager accessDecisionManager;
#Autowired
private ApplicationEventPublisher eventPublisher;
#Autowired
private CsrfTokenRepository csrfTokenRepository;
#Override
protected void configure(HttpSecurity http) throws Exception {
//#formatter:off
http
.headers()
.cacheControl()
.and()
.and()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.and()
.rememberMe()
.key("myKey")
.tokenValiditySeconds(60*60*24*7)
.userDetailsService(memberUserDetailsService)
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.and()
.formLogin()
.loginProcessingUrl("/api/signin")
.failureHandler(authenticationFailureHandler())
.successHandler(authenticationSuccessHandler())
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/api/signout"))
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.addFilter(usernamePasswordAuthenticationFilter())
.addFilter(rememberMeAuthenticationFilter())
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
.authorizeRequests()
.accessDecisionManager(accessDecisionManager)
.antMatchers("/resources/**", "/**").permitAll()
.anyRequest().authenticated();
//#formatter:on
}
private LogoutSuccessHandler logoutSuccessHandler() {
return new LogoutSuccessHandler() {
#Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setStatus(HttpStatus.OK.value());
}
};
}
private AccessDeniedHandler accessDeniedHandler() {
return new AccessDeniedHandler() {
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// TODO: deal with InvalidCsrfTokenException & MissingCsrfTokenException
response.setStatus(HttpStatus.FORBIDDEN.value());
}
};
}
private AuthenticationFailureHandler authenticationFailureHandler() {
return new AuthenticationFailureHandler() {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
};
}
private AuthenticationSuccessHandler authenticationSuccessHandler() {
return new AuthenticationSuccessHandler() {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setStatus(HttpStatus.OK.value());
Member member = (Member) authentication.getPrincipal();
eventPublisher.publishEvent(new SigninApplicationEvent(member));
response.setStatus(HttpStatus.OK.value());
// TODO: overhaul below
response.addHeader("MEMBER_ROLE", member.getRole().name());
}
};
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(rememberMeAuthenticationProvider()).userDetailsService(memberUserDetailsService).passwordEncoder(passwordEncoder);
}
#Bean
protected CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
#Bean
public RememberMeAuthenticationProvider rememberMeAuthenticationProvider() {
return new RememberMeAuthenticationProvider("myKey");
}
#Bean
public RememberMeServices rememberMeServices() {
return new TokenBasedRememberMeServices("myKey", memberUserDetailsService);
}
#Bean
public RememberMeAuthenticationFilter rememberMeAuthenticationFilter() throws Exception {
return new RememberMeAuthenticationFilter(authenticationManager(), rememberMeServices());
}
#Bean
public UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
UsernamePasswordAuthenticationFilter filter = new UsernamePasswordAuthenticationFilter();
filter.setRememberMeServices(rememberMeServices());
filter.setAuthenticationManager(authenticationManager());
return filter;
}
}
Since you have not specified the remember-me service implementation type, TokenBasedRememberMeServices is used by default.
Please find the below note from the documentation when using TokenBasedRememberMeServices:
Don't forget to add your RememberMeServices implementation to your
UsernamePasswordAuthenticationFilter.setRememberMeServices() property,
include the RememberMeAuthenticationProvider in your
AuthenticationManager.setProviders() list, and add
RememberMeAuthenticationFilter into your FilterChainProxy (typically
immediately after your UsernamePasswordAuthenticationFilter)
You need to make the following changes:
In configure() method you need to add a key and filters
http.rememberMe().key("yourKey")
.addFilter(usernamePasswordAuthenticationFilter())
.addFilter(rememberMeAuthenticationFilter())
Create UsernamePasswordAuthenticationFilter and RememberMeAuthenticationFilter
#Bean
public UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter()
throws Exception {
UsernamePasswordAuthenticationFilter filter =
new UsernamePasswordAuthenticationFilter();
filter.setRememberMeServices(memberUserDetailsService);
filter.setAuthenticationManager(authenticationManager());
return filter;
}
#Bean
public RememberMeAuthenticationFilter rememberMeAuthenticationFilter()
throws Exception {
RememberMeAuthenticationFilter filter =
new RememberMeAuthenticationFilter(authenticationManager(), memberUserDetailsService);
return filter;
}
Add RememberMeAuthenticationProvider to the list of providers:
#Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(memberUserDetailsService)
.passwordEncoder(passwordEncoder)
.and()
.authenticationProvider(new RememberMeAuthenticationProvider("yourKey"));
}

Resources