I have a Spring Boot application and the web security config looks like as follows:
#Configuration
#EnableWebSecurity(debug = false)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ClientRegistrationRepository clientRegistrationRepository;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// enables oauth2 login for any request
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
// configures logout adding an oidc logout success handler and invalidating the session on logout
.and()
.logout()
.logoutSuccessHandler(oidcLogoutSuccessHandler())
.invalidateHttpSession(true)
// adds security headers
.and()
.headers()
.addHeaderWriter(new StaticHeadersWriter("Referrer-Policy", "same-origin"))
.addHeaderWriter(new StaticHeadersWriter("Strict-Transport-Security", "max-age=31536000; includeSubDomains"))
// configures csrf
.and()
.csrf()
.csrfTokenRepository(getCookieCsrfTokenRepository());
}
#Bean
public WebSecurityCustomizer webSecurityCustomizer() {
// disables security for swagger ui and open api docs
return web -> web.ignoring().antMatchers("/**/health", "/swagger-ui/**", "/v3/**");
}
private CookieCsrfTokenRepository getCookieCsrfTokenRepository() {
CookieCsrfTokenRepository cookieCsrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
cookieCsrfTokenRepository.setCookiePath("/");
return cookieCsrfTokenRepository;
}
private OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler() {
// configures the oidc logout success handler to redirect to the idp login page
OidcClientInitiatedLogoutSuccessHandler successHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
successHandler.setPostLogoutRedirectUri("{baseUrl}" + RouteConstants.IDP_LOGIN);
return successHandler;
}
}
After updating the Spring Boot version from 2.7 to 3.0.1 and doing according changes, the applications runs normally I can login successfully, but the logout doesn't work. The WebSecurityConfig class now looks like as follows:
#Configuration
#EnableWebSecurity(debug = false)
public class WebSecurityConfig {
#Autowired
private ClientRegistrationRepository clientRegistrationRepository;
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// enables oauth2 login for any request
.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
// configures logout adding an oidc logout success handler and invalidating the session on logout
.and()
.logout()
.logoutSuccessHandler(oidcLogoutSuccessHandler())
.invalidateHttpSession(true)
// adds security headers
.and()
.headers()
.addHeaderWriter(new StaticHeadersWriter("Referrer-Policy", "same-origin"))
.addHeaderWriter(new StaticHeadersWriter("Strict-Transport-Security", "max-age=31536000; includeSubDomains"))
// configures csrf
.and()
.csrf()
.csrfTokenRepository(getCookieCsrfTokenRepository());
return http.build();
}
//The rest of the methods are the same
}
I added this property to make paths starting with /** work in a method webSecurityCustomizer()
spring.mvc.pathmatch.matching-strategy=ant-path-matcher
I noticed that the response cookie of http://localhost/api/v1/authentication/idp_login request doesn't contain XSRF-TOKEN, but before the update it was. Looking at the logs I am getting this error
org.springframework.security.web.FilterChainProxy - Securing POST /logout
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking DisableEncodeUrlFilter (1/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking WebAsyncManagerIntegrationFilter (2/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking SecurityContextHolderFilter (3/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking HeaderWriterFilter (4/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking CsrfFilter (5/15)
2023-01-21 DEBUG org.springframework.security.web.csrf.CsrfFilter - Invalid CSRF token found for http://localhost/api/v1/authentication/logout
2023-01-21 DEBUG org.springframework.security.web.access.AccessDeniedHandlerImpl - Responding with 403 status code
2023-01-21 TRACE org.springframework.security.web.header.writers.HstsHeaderWriter - Not injecting HSTS header since it did not match request to [Is Secure]
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Trying to match request against DefaultSecurityFilterChain [RequestMatcher=Mvc [pattern='/**/health'], Filters=[]] (1/4)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Trying to match request against DefaultSecurityFilterChain [RequestMatcher=Mvc [pattern='/swagger-ui/**'], Filters=[]] (2/4)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Trying to match request against DefaultSecurityFilterChain [RequestMatcher=Mvc [pattern='/v3/**'], Filters=[]] (3/4)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter#527fc8e, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#61bfc9bf, org.springframework.security.web.context.SecurityContextHolderFilter#3722c145, org.springframework.security.web.header.HeaderWriterFilter#7601bc96, org.springframework.security.web.csrf.CsrfFilter#33063f5b, org.springframework.security.web.authentication.logout.LogoutFilter#3291b443, org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter#707b1a44, org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter#7132a9dc, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter#424de326, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter#2c7106d9, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#2975a9e, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#765ffb14, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#329bad59, org.springframework.security.web.access.ExceptionTranslationFilter#33634f04, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#53abfc07]] (4/4)
2023-01-21 DEBUG org.springframework.security.web.FilterChainProxy - Securing POST /error
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking DisableEncodeUrlFilter (1/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking WebAsyncManagerIntegrationFilter (2/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking SecurityContextHolderFilter (3/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking HeaderWriterFilter (4/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking CsrfFilter (5/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking LogoutFilter (6/15)
2023-01-21 TRACE org.springframework.security.web.authentication.logout.LogoutFilter - Did not match request to Ant [pattern='/logout', POST]
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking OAuth2AuthorizationRequestRedirectFilter (7/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking OAuth2LoginAuthenticationFilter (8/15)
2023-01-21 TRACE org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter - Did not match request to Ant [pattern='/login/oauth2/code/*']
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking DefaultLoginPageGeneratingFilter (9/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking DefaultLogoutPageGeneratingFilter (10/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking RequestCacheAwareFilter (11/15)
2023-01-21 TRACE org.springframework.security.web.savedrequest.HttpSessionRequestCache - matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking SecurityContextHolderAwareRequestFilter (12/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking AnonymousAuthenticationFilter (13/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking ExceptionTranslationFilter (14/15)
2023-01-21 TRACE org.springframework.security.web.FilterChainProxy - Invoking FilterSecurityInterceptor (15/15)
2023-01-21 TRACE org.springframework.security.web.context.HttpSessionSecurityContextRepository - Retrieved SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=Name: [f:5f27139d-e373-4f94-885e-2771b67c493e:admin], Granted Authorities: [[OIDC_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]], User Attributes: [{at_hash=IezFNMq7qXBpS499wXpjZg, sub=f:5f27139d-e373-4f94-885e-2771b67c493e:admin, email_verified=false, iss=http://localhost:80/idp/realms/products, typ=ID, preferred_username=admin, nonce=RpXazbXIYWDYNmAYJeX088axKakz0J-XpIq7KoYzHGs, platform={userId=db1ca9cc-da2e-4e6a-bd96-111111111111, username=admin, email=admin#fortra.tmp.com, tenantId=bf045e23-d197-4ba2-ba5b-1f1f275ac2cd, cpUserId=cp_user_id2}, sid=9a579605-b6aa-4267-91e9-d6cfde2bbc4d, aud=[cp], acr=1, azp=cp, auth_time=2023-01-21T19:54:16Z, exp=2023-01-21T19:59:17Z, session_state=9a579605-b6aa-4267-91e9-d6cfde2bbc4d, iat=2023-01-21T19:54:26Z, jti=7278704f-2b56-471a-9b11-ef017268de23}], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=172.27.0.11, SessionId=9fa00c79-63cf-4528-bf1f-c0d358d9b655], Granted Authorities=[OIDC_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]]] from SPRING_SECURITY_CONTEXT
2023-01-21 TRACE org.springframework.security.web.authentication.AnonymousAuthenticationFilter - Did not set SecurityContextHolder since already authenticated OAuth2AuthenticationToken [Principal=Name: [f:5f27139d-e373-4f94-885e-2771b67c493e:admin], Granted Authorities: [[OIDC_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]], User Attributes: [{at_hash=IezFNMq7qXBpS499wXpjZg, sub=f:5f27139d-e373-4f94-885e-2771b67c493e:admin, email_verified=false, iss=http://localhost:80/idp/realms/products, typ=ID, preferred_username=admin, nonce=RpXazbXIYWDYNmAYJeX088axKakz0J-XpIq7KoYzHGs, platform={userId=db1ca9cc-da2e-4e6a-bd96-111111111111, username=admin, email=admin#fortra.tmp.com, tenantId=bf045e23-d197-4ba2-ba5b-1f1f275ac2cd, cpUserId=cp_user_id2}, sid=9a579605-b6aa-4267-91e9-d6cfde2bbc4d, aud=[cp], acr=1, azp=cp, auth_time=2023-01-21T19:54:16Z, exp=2023-01-21T19:59:17Z, session_state=9a579605-b6aa-4267-91e9-d6cfde2bbc4d, iat=2023-01-21T19:54:26Z, jti=7278704f-2b56-471a-9b11-ef017268de23}], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=172.27.0.11, SessionId=9fa00c79-63cf-4528-bf1f-c0d358d9b655], Granted Authorities=[OIDC_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]]
2023-01-21 TRACE org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Did not re-authenticate OAuth2AuthenticationToken [Principal=Name: [f:5f27139d-e373-4f94-885e-2771b67c493e:admin], Granted Authorities: [[OIDC_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]], User Attributes: [{at_hash=IezFNMq7qXBpS499wXpjZg, sub=f:5f27139d-e373-4f94-885e-2771b67c493e:admin, email_verified=false, iss=http://localhost:80/idp/realms/products, typ=ID, preferred_username=admin, nonce=RpXazbXIYWDYNmAYJeX088axKakz0J-XpIq7KoYzHGs, platform={userId=db1ca9cc-da2e-4e6a-bd96-111111111111, username=admin, email=admin#fortra.tmp.com, tenantId=bf045e23-d197-4ba2-ba5b-1f1f275ac2cd, cpUserId=cp_user_id2}, sid=9a579605-b6aa-4267-91e9-d6cfde2bbc4d, aud=[cp], acr=1, azp=cp, auth_time=2023-01-21T19:54:16Z, exp=2023-01-21T19:59:17Z, session_state=9a579605-b6aa-4267-91e9-d6cfde2bbc4d, iat=2023-01-21T19:54:26Z, jti=7278704f-2b56-471a-9b11-ef017268de23}], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=172.27.0.11, SessionId=9fa00c79-63cf-4528-bf1f-c0d358d9b655], Granted Authorities=[OIDC_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]] before authorizing
2023-01-21 TRACE org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Authorizing filter invocation [POST /error] with attributes [authenticated]
2023-01-21 DEBUG org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Authorized filter invocation [POST /error] with attributes [authenticated]
2023-01-21 TRACE org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Did not switch RunAs authentication since RunAsManager returned null
2023-01-21 DEBUG org.springframework.security.web.FilterChainProxy - Secured POST /error
Do the other changes need to make it work?
I resolved the problem by adding csrfTokenRequestHandler like this
http.csrf().csrfTokenRequestHandler(getCsrfTokenRequestHandler())
private CsrfTokenRequestAttributeHandler getCsrfTokenRequestHandler() {
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName(null);
return request handler;
}
Still had another issue that x-xss-protection is 0 but should be "1; mode=block"
I use spring security, I try to allow a post without need to be connected.
So in a class who extends WebSecurityConfigurerAdapter
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers(
"/",
"/help**",
"/css/**",
"/js/**",
"/img/**").permitAll()
.antMatchers(HttpMethod.POST, "/printdownload**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").permitAll()
.successHandler(customAuthenticationSuccessHandler)
.and()
.logout();
}
When I try to call this controller
#PostMapping("/printdownload")
public String printTestament(#RequestBody TestamentWizard testamentDocuement){
....
}
I get this
o.s.security.web.FilterChainProxy : Securing POST /printdownload
s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/printdownload
o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code
w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
o.s.security.web.FilterChainProxy : Securing POST /error
s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
o.s.s.w.a.i.FilterSecurityInterceptor : Failed to authorize filter invocation [POST /error] with attributes [authenticated]
o.s.s.web.DefaultRedirectStrategy : Redirecting to http://localhost:8080/login
w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
o.s.security.web.FilterChainProxy : Securing GET /login
s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
o.s.s.w.a.i.FilterSecurityInterceptor : Authorized filter invocation [GET /login] with attributes [permitAll]
o.s.security.web.FilterChainProxy : Secured GET /login
w.c.HttpSessionSecurityContextRepository : Did not store anonymous SecurityContext
w.c.HttpSessionSecurityContextRepository : Did not store anonymous SecurityContext
s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
Just don't understand why post is secured when i specified to permit it
Spring Security secures requests through defense in depth. Because of this, access rules like permitAll() and authenticated() are not the only thing protecting the web application.
In your case, CSRF protection is rejecting the request because it is enabled by default along with other protection against exploits. You can read about CSRF protection and why POST requests require a CSRF token in the reference docs.
I'm trying to make an authenticated GET request on one of the resources:
http://user:psw#localhost:8090/devices
This works fine from the browser. But from National Instrument GWeb I keep getting Code 401 (Unauthorized).
SecurityConfiguration.java:
#Configuration
#EnableWebSecurity
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final DatabaseUserDetailsService databaseUserDetailsService;
public SecurityConfiguration(DatabaseUserDetailsService databaseUserDetailsService) {
super();
this.databaseUserDetailsService = databaseUserDetailsService;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.cors().and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
#Bean
public AuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(this.databaseUserDetailsService);
return provider;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://rog-valerio", "http://localhost:8090"));
configuration.setAllowedMethods(Arrays.asList("GET","POST", "OPTIONS"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
From the configure() method:
httpSecurity.cors().and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic();
I'm I am not wrong this should mean that any request should be able to authenticate.
By enabling spring security debug, when I try to make the authenticated request I get the following:
2022-03-09 10:37:00.520 DEBUG 27408 --- [nio-8090-exec-5] o.s.security.web.FilterChainProxy : Securing GET /devices
2022-03-09 10:37:00.520 DEBUG 27408 --- [nio-8090-exec-5] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2022-03-09 10:37:00.521 DEBUG 27408 --- [nio-8090-exec-5] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
2022-03-09 10:37:00.521 DEBUG 27408 --- [nio-8090-exec-5] o.s.s.w.a.i.FilterSecurityInterceptor : Failed to authorize filter invocation [GET /devices] with attributes [authenticated]
2022-03-09 10:37:00.522 DEBUG 27408 --- [nio-8090-exec-5] o.s.s.w.s.HttpSessionRequestCache : Saved request http://localhost:8090/devices to session
2022-03-09 10:37:00.523 DEBUG 27408 --- [nio-8090-exec-5] s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]
2022-03-09 10:37:00.523 DEBUG 27408 --- [nio-8090-exec-5] s.w.a.DelegatingAuthenticationEntryPoint : No match found. Using default entry point org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint#30dfc62d
2022-03-09 10:37:00.523 DEBUG 27408 --- [nio-8090-exec-5] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-03-09 10:37:00.523 DEBUG 27408 --- [nio-8090-exec-5] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-03-09 10:37:00.523 DEBUG 27408 --- [nio-8090-exec-5] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2022-03-09 10:37:00.523 DEBUG 27408 --- [nio-8090-exec-5] o.s.security.web.FilterChainProxy : Securing GET /error
2022-03-09 10:37:00.524 DEBUG 27408 --- [nio-8090-exec-5] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2022-03-09 10:37:00.524 DEBUG 27408 --- [nio-8090-exec-5] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
2022-03-09 10:37:00.524 DEBUG 27408 --- [nio-8090-exec-5] o.s.security.web.FilterChainProxy : Secured GET /error
2022-03-09 10:37:00.525 DEBUG 27408 --- [nio-8090-exec-5] a.DefaultWebInvocationPrivilegeEvaluator : filter invocation [/error] denied for AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=05282221D24CA222616679CE3049C092], Granted Authorities=[ROLE_ANONYMOUS]]
org.springframework.security.access.AccessDeniedException: Access is denied
And access is denied. Username and password are correct. Why am I getting the request rejected? Maybe there is some configuration that I am missing?
I found the answer, configuration was fine. But, as stated here https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization#examples , I added "Authorization" header with base64 encoded username and password. Now it works.
I'll not delete the question because maybe it'll be useful to somebody
I have a Spring Boot application with two security configurations (two WebSecurityConfigurerAdapters), one for a REST API with "/api/**" endpoints, and one for a web front-end at all other endpoints. The security configuration is here on Github and here's some relevant parts of it:
#Configuration
#Order(1)
public static class APISecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(authenticationManager());
jwtAuthenticationFilter.setFilterProcessesUrl("/api/login");
jwtAuthenticationFilter.setPostOnly(true);
http.antMatcher("/api/**")
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilter(jwtAuthenticationFilter)
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
}
#Configuration
public static class FrontEndSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login").permitAll()
.defaultSuccessUrl("/")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/?logout")
.and()
.authorizeRequests()
.mvcMatchers("/").permitAll()
.mvcMatchers("/home").authenticated()
.anyRequest().denyAll()
.and();
}
}
The JWTAuthenticationFilter is a custom subclass of UsernamePasswordAuthenticationFilter that processes sign-in attempts to the REST API (by HTTP POST with a JSON body to /api/login) and returns a JWT token in the "Authorization" header if successful.
So here's the issue: failed login attempts to /api/login (either with bad credentials or missing JSON body) are redirecting to the HTML login form /api/login. Non-authenticated requests to other "/api/**" endpoints result in a simple JSON response such as:
{
"timestamp": "2019-11-22T21:03:07.892+0000",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/api/v1/agency"
}
{
"timestamp": "2019-11-22T21:04:46.663+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/api/v1/badlink"
}
Attempts to access other protected URLs (not starting with "/api/") by a non-authenticated user redirect to the login form /login, which is the desired behavior. But I don't want API calls to /api/login to redirect to that form!
How can I code the correct behavior for failed API logins? Is it a question of adding a new handler for that filter? Or maybe adding an exclusion to some behavior I've already defined?
More detail on the exception and handling:
I looked at the logs, and the exception being thrown for either bad credentials or malformed JSON is a subclass of org.springframework.security.authentication.AuthenticationException. The logs show, for example:
webapp_1 | 2019-11-25 15:30:16.048 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter : Authentication request failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
(...stack trace...)
webapp_1 | 2019-11-25 15:30:16.049 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter : Updated SecurityContextHolder to contain null Authentication
webapp_1 | 2019-11-25 15:30:16.049 DEBUG 1 --- [nio-8080-exec-5] n.j.w.g.config.JWTAuthenticationFilter : Delegating to authentication failure handler org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler#7f9648b6
webapp_1 | 2019-11-25 15:30:16.133 DEBUG 1 --- [nio-8080-exec-6] n.j.webapps.granite.home.HomeController : Accessing /login page.
When I access another URL, for example one that doesn't exist such as /api/x, it looks very different. It's pretty verbose but it looks like the server is trying to redirect to /error and is not finding that to be an authorized URL. Interestingly if I try this in a web browser I get the error formatted with my custom error page (error.html), but if I access it with Postman I just get a JSON message. A sample of the logs:
webapp_1 | 2019-11-25 16:07:22.157 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point
webapp_1 |
webapp_1 | org.springframework.security.access.AccessDeniedException: Access is denied
...
webapp_1 | 2019-11-25 16:07:22.174 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.ExceptionTranslationFilter : Calling Authentication entry point.
webapp_1 | 2019-11-25 16:07:22.175 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
...
webapp_1 | 2019-11-25 16:07:22.211 DEBUG 1 --- [nio-8080-exec-6] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : '/error'; against '/api/**'
...
webapp_1 | 2019-11-25 16:07:22.214 DEBUG 1 --- [nio-8080-exec-6] o.s.security.web.FilterChainProxy : /error reached end of additional filter chain; proceeding with original chain
webapp_1 | 2019-11-25 16:07:22.226 DEBUG 1 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error", parameters={}
webapp_1 | 2019-11-25 16:07:22.230 DEBUG 1 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
webapp_1 | 2019-11-25 16:07:22.564 DEBUG 1 --- [nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json]
webapp_1 | 2019-11-25 16:07:22.577 DEBUG 1 --- [nio-8080-exec-6] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Mon Nov 25 16:07:22 GMT 2019, status=403, error=Forbidden, message=Access Denied, path=/a (truncated)...]
webapp_1 | 2019-11-25 16:07:22.903 DEBUG 1 --- [nio-8080-exec-6] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
webapp_1 | 2019-11-25 16:07:22.905 DEBUG 1 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 403
So it looks like what I maybe need to do is to configure the "authentication failure handler" for the REST API to go to "/error" instead of going to "/login", but only for endpoints under /api/**.
I added an #Override of unsuccessfulAuthentication() to my Authentication filter
The grandparent class (AbstractAuthenticationProcessingFilter) has a method for unsuccessful authentications (i.e. AuthenticationException) which delegates to an authentication failure handler class. I could have created my own custom authentication failure handler, but instead decided to simply override the unsuccessfulAuthentication method with some code that sends back a response with a 401 status and a JSON error message:
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// TODO: enrich/improve error messages
response.setStatus(response.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.getWriter().write("{\"error\": \"authentication error?\"}");
}
...and added a custom AuthenticationEntryPoint to make the other errors match
This doesn't have the exact form of the error messages I had seen at other endpoints, so I also created a custom AuthenticationEntryPoint (the class that handles unauthorized requests to protected endpoints) and it does basically the same thing. My implementation:
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// TODO: enrich/improve error messages
response.setStatus(response.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.getWriter().write("{\"error\": \"unauthorized?\"}");
}
}
Now the security configuration for the REST endpoints looks like this (note the addition of ".exceptionHandling().authenticationEntryPoint()":
#Configuration
#Order(1)
public static class APISecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(authenticationManager());
jwtAuthenticationFilter.setFilterProcessesUrl("/api/login");
http.antMatcher("/api/**")
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(new RESTAuthenticationEntryPoint())
.and()
.addFilter(jwtAuthenticationFilter)
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
}
I'll need to work on my error messages so they're more informative and secure.
And this doesn't exactly answer my original question, which was how to get those default-style JSON responses I liked, but it does allow me to customize the errors from all types of access failures independently of the web configuration. (Endpoints outside of /api/** still work with the web form login.)
Full code as of the current commit: github
We have a CustomAuthenticationProvider(AuthenticationProvider) developed for Spring which works with CustomAuthenticationRequest(Authentication), CustomAuthentication(Authentication), a CustomUser.
Once we validate credentials when our Controller is invoked we create a CustomAuthenticationRequest based on the credentials.
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(new CustomAuthenticationRequest(new CustomUser(account.getUsername())));
Debug login confirms that the CustomAuthenticationRequest has been stored in the HTTPSession.
HttpSessionSecurityContextRepository - SecurityContext 'org.springframework.security.core.context.SecurityContextImpl#730db7d8: Authentication: pro.someplace.spring.CustomAuthenticationRequest#730db7d8' stored to HttpSession: 'org.apache.catalina.session.StandardSessionFacade#5da80010
The WebSecurityConfigurerAdapter registers our AuthenticationProvider:
#Override
public void configure(AuthenticationManagerBuilder builder)
throws Exception {
builder.authenticationProvider(new CustomAuthenticationProvider());
}
And establishes what can and cannot be seen by anonymous and authenticated users.
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/login", "/registration").permitAll()
.anyRequest()
.authenticated();
}
The problem we have is that before the FilterSecurityInterceptor can consult which AuthenticationProvider is appropriate the AnonymousAuthenticationFilter steps in:
o.s.s.w.a.AnonymousAuthenticationFilter - Populated SecurityContextHolder with anonymous token: 'org.springframework.security.authentication.AnonymousAuthenticationToken#4cc1f847: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#fffd148a: RemoteIpAddress: 127.0.0.1; SessionId: 74DB809F1CB5CFB1F977EC20B37B218E; Granted Authorities: ROLE_ANONYMOUS'
If I remove the AnonymousAuthenticationFilter then I cannot access permitAll() in the configuration (different error).
Curiously, I notice this logging message at the end of request processing:
SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed
Ok. So the SecurityContextPersistenceFilter should have persisted the context in the HttpSessionSecurityContextRepository.
But when the next request appears the SecurityContextPersistenceFilter has no such object. Was it saved at all? Was it removed?
o.s.security.web.FilterChainProxy - /ordervalidator at position 2 of 10 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
o.s.s.w.c.HttpSessionSecurityContextRepository - No HttpSession currently exists
o.s.s.w.c.HttpSessionSecurityContextRepository - No SecurityContext was available from the HttpSession: null. A new one will be created.
How can I configure spring to allow authenticated users where I want them and use my CustomAuthenticationProvider when available in the HTTPSession? Where is the security object and why is it not being stored?
And so will be as dumb as it sounds. The front end is React and so we were returning a cookie to the front end however the front end was not being returned to us. As soon as I manually insert cookies into requests I have trapped in either (Postman or Burp) my Context can be found.
This is a cautionary tale as there is a generational difference between the frontend React and the backend (Spring) developers. The backend expect cookies and this is assumed, the front end "...have no idea why we would want them and its a bad idea".