I have a NextJS application that uses a Backend for Frontend Architecture with Spring Security OAuth2 Client and Spring Cloud Gateway, which communicates to my Spring Authorization Server, very similar to this sample.
My webapp is working real nice, I'm getting the SESSION and X-CSRF token from my BFF and are being set in the browser on my NextJS app as cookies, so everything is cool to that point. But my doubt is that I closed the browser window and my session goes away, obviously it happens since both the cookies have MAX-AGE as "Session".
I know that the best practice is to let is as is, let the session either expire by the session timeout or when the browser session ends, but I'm curious to know how to persist the SESSION and X-CSRF cookies after the browser closes, so I have these questions:
Is it just enough to set the MAX-AGE to something in both the BFF and Spring Authorization Server?
Is Spring Security Remember Me needed? Though my BFF uses WebFlux Security so that functionality isn't available.
Should the X-CSRF Cookie also be persisted after the browser is closed, just as the session?
Should the session timeout equal the max age that I would set for both the cookies?
Should the X-CSRF token be persisted in a database if I spun up multiple instances of the BFF?
Also I'm confused on how to setup this because of the fact that I do the login on the Spring Authorization Server but I'm also logged in in the BFF since I have the SESSION and X-CSRF token to communicate with my BFF, so I guess that both session configuration should be the same on these two apps since they both create a session cookie even though the browser only gets the BFF one.
Also worth noting that both my BFF and my Spring Authorization Server, use Spring Session with Redis using different namespaces.
Relevant Spring Security configuration in my BFF:
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(authorizeExchange ->
authorizeExchange.anyExchange().authenticated()
)
.exceptionHandling(exceptionHandling ->
exceptionHandling.authenticationEntryPoint(authenticationEntryPoint())
)
.csrf(csrf ->
csrf.csrfTokenRepository(csrfTokenRepository())
)
.cors(Customizer.withDefaults())
.oauth2Login(oauth2 ->
oauth2.authenticationSuccessHandler(authenticationSuccessHandler())
)
.logout(logout ->
logout
.logoutHandler(logoutHandler())
.logoutSuccessHandler(logoutSuccessHandler())
)
.oauth2Client(Customizer.withDefaults());
return http.build();
}
Security Configuration on my Spring Authorization Server:
// AuthorizationServerConfig class
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.cors(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.build();
}
// WebSecurityConfig class
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.cors(withDefaults())
.formLogin(withDefaults());
return http.build();
}
I've done some research and for folks looking for something similar this may shed a little bit of light.
To answer my own questions:
Is it just enough to set the MAX-AGE to something in both the BFF and Spring Authorization Server?
I think that setting the max-age for my BFF for my session and x-csrf token is enough for enabling my users to be able to open my SPA again and be able to keep using the application freely without login in again.
You can find more information about how to set the max age when using Spring Security for servlet applications here or here for Reactive applications
I don't think that it makes sense to set the max age for the Spring Authorization Server microservice since the browser only gets and needs the one from the BFF Gateway microservice.
Also it is worth noting that I would guess that the session timeout should be equal or superior for what you set the max age, since when your users are away from the app the inactivity on the BFF would invalidate the session if it timeouts, more on that here
Is Spring Security Remember Me needed? Though my BFF uses WebFlux Security so that functionality isn't available.
If you have a servlet application, Remember me does allow to auto login the user when they close the window back, in my case I use a Reactive application therefore this feature is not yet built in. But if yours is a servlet one, you can try the feature here
Should the X-CSRF Cookie also be persisted after the browser is closed, just as the session?
I would think so, because it won't be generated back since you aren't automatically login on the app when you reopen the browser window, you are just still using the same session. This sounds like a bad practice but I haven't found what to do in this case.
You can set the max age for the X-CSRF token on both servlet and reactive applications by using CookieCsrfTokenRepository or CookieServerCsrfTokenRepository.
Should the session timeout equal the max age that I would set for both the cookies?
Again I would think that the timeout should be equal or superior or depending of the time you would like to give the users to make time it out, since it refreshes every time someone does something on the server. Look more on that here
Should the X-CSRF token be persisted in a database if I spun up multiple instances of the BFF?
I don't think so, since it appears that it's tied to the HTTPSession and you are using something like Spring Session and already storing that on the database, then I don't think that you should try to store it in a different way. More on that here
If anyone wants to add something more, or I said something wrong please correct it.
Related
Given a hypothetical application which uses Spring Session to store session information and:
There are more than one way of initiating a session and authenticating, i.e. different endpoints that can be hit depending on how the user is "logging in".
All of the endpoints a user can use for authentication result in a session attribute called "authenticated" being set to true.
Is it possible to configure Spring Security to determine whether a request is authenticated based on the presence and truthiness of that session variable?
The security filter chain might look something like this
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.cors()
.authorizeHttpRequests(
requests -> {
requests.antMatchers("/auth/login").permitAll();
requests.antMatchers("/auth/sso-login").permitAll();
requests.antMatchers("/auth/developer-login").permitAll();
requests.anyRequest().authenticated();
})
.build();
}
The idea would be that so long as a user has hit any of the login endpoints correctly, the application flags the session as authenticated, allowing the user to access other endpoints as an authenticated user.
Or is there a more integrated solution that allows the application to designate a particular session as authenticated, not using traditional mechanisms like BasicAuth? Specifically in the case of a developer utility being able to mock a login as a mocked user without providing credentials. Simply hitting the endpoint (in the environments where it is available) triggering a fully authenticated session as far as Spring Security is concerned.
I am using Spring Security along with Spring Authorization Server and experimenting with creating an auth server.
I have a basic flow allowing me to login with the pre-built login page (from a baledung guide - this is the code I'm working off ). I'm assuming this login page form comes from formLogin() like so:
http.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
//.formLogin(withDefaults());
return http.build();
I would like to not use this pre-built form as I have a need to host and run the login form front-end application completely separately. ie on a different server, domain and codebase.
Another way to ask this question could be:
How do I disable the built in form in authorization-server so I can use it with a completely separate form?
Are there any recommended ways of learning about how customise my SecurityFilterChain along these lines? Is this the correct place to look? I find the baledung article (and articles like that) helpful as a starting point, but seldom works for more practical use case. I'm confident Spring Security and the oauth2 libraries will allow me to do what I want, but not entirely clear.
After discussing this with you, I've gathered that what you're trying to do is essentially pre-authenticate the user that was authenticated through another (separately hosted) login page, actually a separate system. The idea is that the other system would redirect back with a signed JWT in a query parameter.
This really becomes more of a federated login problem at that point, which is what SAML 2.0 and OAuth 2.0 are aimed at solving. However, if you have to stick with things like a signed JWT (similar to a SAML assertion), we could model a fairly simple pre-authenticated authorization_code flow using the Spring Authorization Server.
Note: I haven't explored options for JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants but it could be a viable alternative. See this issue (#59).
Additional note: There are numerous security considerations involved with the approach outlined below. What follows is a sketch of the approach. Additional considerations include CSRF protection, using Form Post Response Mode (similar to SAML 2.0) to protect the access token instead of a query parameter, aggressively expiring the access token (2 minutes or less), and others. In other words, using a federated login approach like SAML 2.0 or OAuth 2.0 will always be RECOMMENDED over this approach when possible.
You could to start with the existing Spring Authorization Server sample and evolve it from there.
Here's a variation that redirects to an external authentication provider and includes a pre-authentication mechanism on the redirect back:
#Bean
#Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// #formatter:off
http
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("https://some-other-sso.example/login"))
);
// #formatter:on
return http.build();
}
#Bean
#Order(2)
public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
// #formatter:off
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
// #formatter:on
return http.build();
}
#Bean
public JwtDecoder jwtDecoder(PublicKey publicKey) {
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) publicKey).build();
}
#Bean
public BearerTokenResolver bearerTokenResolver() {
DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
bearerTokenResolver.setAllowUriQueryParameter(true);
return bearerTokenResolver;
}
The first filter chain operates on authorization server endpoints, such as /oauth2/authorize, /oauth2/token, etc. Note that the /oauth2/authorize endpoint requires an authenticated user to function, meaning that if the endpoint is invoked, the user has to be authenticated, or else the authentication entry point is invoked, which redirects to the external provider. Also note that there must be a trusted relationship between the two parties, since we're not using OAuth for the external SSO.
When a redirect from the oauth client comes to the /oauth2/authorize?... endpoint, the request is cached by Spring Security so it can be replayed later (see controller below).
The second filter chain authenticates a user with a signed JWT. It also includes a customized BearerTokenResolver which reads the JWT from a query parameter in the URL (?access_token=...).
The PublicKey injected into the JwtDecoder would be from the external SSO provider, so you can plug that in however it makes sense to in your setup.
We can create a stub authentication endpoint that converts the signed JWT into an authenticated session on the authorization server, like this:
#Controller
public class SsoController {
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
#GetMapping("/login")
public void login(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
this.successHandler.onAuthenticationSuccess(request, response, authentication);
}
}
The .oauth2ResourceServer() DSL causes the user to be authenticated when the /login endpoint is invoked. It requires an access_token parameter (used by the BearerTokenResolver) to pre-authenticate the user by validating the signed JWT as an assertion that the user has been externally authenticated. At this point, a session is created that will authenticate all future requests by this browser.
The controller is then invoked, and simply redirects back to the real authorization endpoint using the SavedRequestAwareAuthenticationSuccessHandler, which will happily initiate the authorization_code flow.
Re your comnent: "I'm attempting to build an Authorization Server":
Coding your own Authorization Server (AS) or having to build its code yourself is highly inadvisable, since it is easy to get bogged down in plumbing or to make security mistakes.
By all means use Spring OAuth Security in your apps though. It is hard enough to get these working as desired, without taking on extra work.
SUGGESTED APPROACH
Choose a free AS and run it as a Docker Container, then connect to its endpoints from your apps.
If you need to customize logins, use a plugin model, write a small amount of code, then deploy a JAR file or two to the Docker container.
This will get you up and running very quickly. Also, since Spring Security is standards based, you are free to change your mind about providers, and defer decisions on the final one.
EXAMPLE IMPLEMENTATION
Curity, along with other good choices like Keycloak or Ory Hydra are Java based and support plugins:
Curity Community Edition
Custom Authenticator Example
I am building a Spring Boot application using version 2.3.4 with spring-boot-starter-oauth2-client and spring-boot-starter-security dependencies.
I am trying to implement the JIRA Tempo plugin OAuth support.
I have it partially working using the following properties:
spring.security.oauth2.client.registration.tempo.redirect-uri=http://localhost:8080
spring.security.oauth2.client.registration.tempo.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.tempo.client-id=<the-client-id>
spring.security.oauth2.client.registration.tempo.client-secret=<the-client-secret>
spring.security.oauth2.client.registration.tempo.provider=tempo
spring.security.oauth2.client.provider.tempo.authorization-uri=https://mycompany.atlassian.net/plugins/servlet/ac/io.tempo.jira/oauth-authorize/?access_type=tenant_user
spring.security.oauth2.client.provider.tempo.token-uri=https://api.tempo.io/oauth/token/
and this config:
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests(expressionInterceptUrlRegistry -> expressionInterceptUrlRegistry.anyRequest().authenticated())
.oauth2Login();
}
When I access http://localhost:8080, it redirects to JIRA/Tempo and shows the approval dialog there to grant access to the Tempo data for my application. I can grant access, but after that, it just redirects again to that page instead of showing my own application.
With debugging, I noticed that there is a redirect to http://localhost:8080/?code=.... but Spring Security is not handling that. What else do I need to configure?
I also tried to set some breakpoints in DefaultAuthorizationCodeTokenResponseClient, but they are never hit.
UPDATE:
I changed the redirect-uri to be:
spring.security.oauth2.client.registration.tempo.redirect-uri={baseUrl}/{action}/oauth2/code/{registrationId}
(and I changed the Redirect URIs setting in Tempo to http://localhost:8080/login/oauth2/code/tempo).
This now redirects back to my localhost, but it fails with authorization_request_not_found.
UPDATE 2:
The reason for the authorization_request_not_found seems to be mismatch in HttpSessionOAuth2AuthorizationRequestRepository.removeAuthorizationRequest(HttpServletRequest request) between what is in the authorizationRequests and the stateParameters.
Note how one ends with = and the other with %3D, which makes them not match. It is probably no coincidence that the URL encoding of = is %3D. It is unclear to me if this is something that is a Spring Security problem, or a problem of the Tempo resource server, or still a misconfiguration on my part.
The redirect-uri property should not point to the root of your application, but to the processing filter, where the code after redirect is processed.
Best practice for you would be to leave the redirect-uri alone for the time being. Then it will default to /login/oauth2/code/* and this Url is handled by org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter.
We have a spring boot application configured as an oauth2 client. Occasionally, we have people where their browser sends a request like this to the application:
https://app/login?code=XXX&state=ZZZ
The code and state were cached from a previous authentication attempt and are invalid right now.
Spring security sees that this person is not authenticated, so it redirects them to /login which then does the whole oauth2 authentication but then after they are authenticated, spring security sends them back to /login?code=XXX&state=ZZZ because that was their original request. When that happens, it tries to validate the code and state but fails and sends them to an error page. This is a problem when supporting the app because the user is authentcated.
Is there a way to change the logic of the the storing of the initial request so that if it is /login we can replace that with /? There might be other solutions we haven't thought of. Any help would be appreciated.
Our app is currently using Spring boot 2 but I've tried this with the latest version of Spring boot 3 and it is still an issue. We have been unable to change the browser behavior so would like to solve this on the server if possible.
Here is our configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/info", "/static/**").permitAll()
.anyRequest().authenticated().and()
.csrf();
}
If I understand you correctly, you want to avoid redirect only sometimes (so SpringSecurity's defaultSuccessUrl is not an option).
If so, you can implement your own AuthenricationSuccessHandler like this:
...
.successHandler(
(request, response, authentication) -> {
if (request.getRequestURI().equals("/your/invalid/path"))
response.sendRedirect("/");
}
...
I'm having a problem with SESSION cookie being reset by Spring Cloud Gateway after a call to a resource server.
I have an Angular application, a Spring Cloud Gateway application, an external Authorization Server and a Resource Server of my own.
The Angular application first authorizes via Spring Cloud Gateway app (who delegates this work to external Authorization Server using OAuth2) and receives a SESSION cookie. At this point the user is authenticated and Authentication object is available in Spring Cloud Gateway app.
Next, the Angular app calls an endpoint of Spring Cloud Gateway app, which actually forwards the call to the Resource Server (and includes the Bearer token in the call, so the call works fine), the Resource server returns some result, which is successfully send back through the Spring Cloud Gateway app to the Angular app. BUT alongside successful response the Spring Cloud Gateway app sends this header:
set-cookie: SESSION=; Max-Age=0; Expires=Sat, 17 Aug 2019 20:39:44 GMT; Path=/; HTTPOnly
which kills the cookie on the client side and makes subsequent calls impossible, even though the Authentication object is still alive and the session looks to fine as well.
Does anyone know what can be the reason of such behavior?
We had the exact issue in our WebFlux resource servers -- the API gateway would proxy a request to a resource server, so the first request worked, but subsequent requests would try to authenticate again because the SESSION cookie was cleared out, resulting in some X-XSRF-TOKEN errors.
We solved this by adding .requestCache().requestCache(NoOpServerRequestCache.getInstance()) to our securityWebFilterChain bean definition in our WebFlux resource servers.
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
class ResourceServerConfiguration {
#Value('${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}')
String jwkSetUri
#Bean
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.csrf().disable()
.requestCache().requestCache(NoOpServerRequestCache.getInstance()).and()
.httpBasic().disable()
.formLogin().disable()
.oauth2ResourceServer().jwt().jwkSetUri(jwkSetUri)
.and().and()
.authorizeExchange()
.pathMatchers("/v1/**").authenticated()
}
}
In the "classic" MVC world, you would configure your ResourceServerConfigurer classes like this:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
Update 11/22/2022
Our microservice architecture has begun to expand, and we started seeing this issue again on services not owned by our team. Turns out they had stateful web services, meaning, sessions were created when calling them which caused the SESSION cookie to get overridden on the Spring Cloud Gateway client application. We applied this to our configuration to permanently resolve this issue:
server:
reactive:
session:
cookie:
name: SESSION_${spring.application.name}
👆 This eliminates an issue where SESSION cookies from other web service calls collide with the Gateway client's SESSION cookie.
I had encountered the same case.
Did the external Authorization Server produce a cookie which is base64 encode?
Such as Set-Cookie: SESSION=YWZjOTc4YmUtOTNmNy00N2UxLTg0NjgtYWJlNWMwZmNiOWUx
If so, it will cause the problem.
The CookieWebSessionIdResolver defined in Spring Web used by Spring Cloud Gateway does not deal with the base64 encoded cookie values. Instead, it directly uses the raw cookie value to find the corresponding session in the storage. Obviously, no Authentication object will be found. So that Spring Cloud Gateway choose to kill the "invalid" cookie given by the Angular app.
There are two solutions given below.
Disable base64 encoding of cookie values in the external Authorization Server if it is also managed by you.
Override WebSessionIdResolver to change the default behaviour so as to decode cookie values when reading by session manager. And don't forget to register it as a Spring Bean in your Spring Cloud Gateway implementation.
In my case, Solution 1 was choosen. My Authorization Server uses Spring Security + Spring Session. I changed the default settings of HttpSessionIdResolver like this.
CookieHttpSessionIdResolver cookieHttpSessionIdResolver = new CookieHttpSessionIdResolver();
DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
defaultCookieSerializer.setUseBase64Encoding(false);
defaultCookieSerializer.setCookieName("SESSION");
cookieHttpSessionIdResolver.setCookieSerializer(defaultCookieSerializer);