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.
Related
I have the following setup:
I'm having an Angular frontend and Spring-Boot backend
Users are logging in to my backend via normal Form login
I'm integrating a third party API which needs oauth2 authentication, so Users need to grant permissions to my App so I can load data on their behalf from that third party
I configured oauth2Client() in my HttpSecurity config to enable oauth2
What currently happens is:
The frontend is calling an endpoint to get data from the third party, lets say /api/get-library which tries to access a protected resource at the third party.
This will lead to a 401 from the third party and trigger the oauth flow in Spring
The User is redirected to the third party to grant permissions to my App
After granting permissions the User is first redirected to the Url I specified as spring.security.oauth2.client.registration.foobar.redirect-uri
Spring Boot then retrieves the token and stores it for my Principal
After that Spring Boot redirects to the original url /api/get-library
But this is just some RestController so the User is presented with some JSON data in the Browser
So point 6 is my problem. I don't want that the User is in the end redirected to some API endpoint, I want him to be redirected to a page of my Angular application.
A similar problem arises if the user rejects the permission grant. Then the user is redirected to spring.security.oauth2.client.registration.foobar.redirect-uri with an query param ?error=true. Also in this case I want a redirect to my Angular application.
Initially I thought I could also configure oauth2Login() which has an failureHandler and successHandler, but those aren't called in my case, since I'm not doing a Login here.
So can somebody help me? How can I configure my own redirects for oauth2Client? In case of success, and on failure? What are relevant Beans here?
I found a solution:
The main Spring class to check is OAuth2AuthorizationCodeGrantFilter. This Filter is invoked when the user granted/rejected the permissions at the OAuth Provider.
Unfortunately there is no way to configure a custom redirect Url for this Filter, so I implemented a hacky solution:
I copied the implementation of OAuth2AuthorizationCodeGrantFilter to an own class and extended it with 2 parameters: success and error return Url. I then used those Urls in the processAuthorizationResponse Method to redirect to my Urls
I then put my ownAppOAuth2AuthorizationCodeGrantFilter before the Spring Filter in the HttpSecurityConfig, so it is used instead of the Spring version
In my Angular App I'm storing the exact location in the App before calling an Endpoint that potentially requires OAuth authentication. So when the User agent returns to the Angular App I can navigate back to the origin location.
It feels very hacky, so if somebody comes up with a better solution I'd be glad to hear it. :-)
Some Code snippets for Spring:
#Override
protected void configure(HttpSecurity http) throws Exception {
...
http.addFilterBefore(oAuth2AuthorizationCodeGrantFilter(), OAuth2AuthorizationCodeGrantFilter.class);
...
}
#Bean #Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public AppOAuth2AuthorizationCodeGrantFilter oAuth2AuthorizationCodeGrantFilter() throws Exception {
return new AppOAuth2AuthorizationCodeGrantFilter(
clientRegistrationRepository,
oAuth2AuthorizedClientRepository,
authenticationManagerBean(),
oauthSuccessRedirectUrl,
oauthErrorRedirectUrl);
}
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.
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 have a Spring Boot application that is "invitation only". Ie. users are sent a signup link and there is no "Sign up" functionality. This works fine and users log on with their username and password.
I would like to allow logon with FaceBook and Google using OAuth2 as a supplementary logon method. This would involve mapping the existing users to their social account in some way. The users and their passwords are stored in a MySQL database. I have found a number of articles on OAuth2 and Spring Boot, but none that address this exact use-case.
I can create the Google OAuth2 token/client secret etc, but how do I design the flow to allow only the existing users to logon with their social accounts?
The usernames have been chosen by the users themselves, and are therefore not necessarily the same as their email.
Do I need to create a custom authentication flow in this case?
And do I need to change the authentication mechanism from cookies to JWT tokens?
I found myself in a similar situation, where I needed to know if the OAuth request that I'm receiving is coming from an already authenticated user or not.
Although in my case I needed to know that because I want users to be able to "link" their existing account to social ones.
What I ended up doing was implementing an OAuth2UserService which would have a single method:
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
// this will be null if the OAuth2UserRequest is not from an authenticated user
// otherwise, it would contain the current user's principle which you can use to check if the OAuth request should be handled or not
Authentication currentAuth = SecurityContextHolder.getContext().getAuthentication();
// for example:
// if(currentAuth == null)
// throw new OAuth2AuthenticationException(OAuth2ErrorCodes.ACCESS_DENIED);
// Use the default service to load the user
DefaultOAuth2UserService defaultService = new DefaultOAuth2UserService();
OAuth2User defaultOAuthUser = defaultService.loadUser(userRequest);
// here you might have extra logic to map the defaultOAuthUser's info to the existing user
// and if you're implementing a custom OAuth2User you should also connect them here and return the custom OAuth2User
return defaultOAuthUser;
}
then just register the custom OAuth2UserService in your security configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// your existing config
.oauth2Login()
.userInfoEndpoint()
.userService(oauthUserService())
;
}
#Bean
public OAuth2UserService oauthUserService(){
return new MyCustomOAuth2UserService();
}
In my application I am using multiple authentication handlers like application DB, LDAP and SAML. Now after successful authentication I am using CustomAuthenticationSuccessHandler.java which extends SimpleUrlAuthenticationSuccessHandler class which will be called after successful authentication. My question is how to get information about which handler has a successful authentication. I need this information because if it is an external user (LDAP, SAML) then I have to write a logic to replicate the user in application DB.
My configuation in configure global method:
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
auth
.ldapAuthentication()
.ldapAuthoritiesPopulator(ldapAuthoritiesPopulator)
.userDnPatterns("uid={0},ou=people")
.userDetailsContextMapper(ldapUserDetailsContextMapper)
.contextSource(getLDAPContextSource());`
You can set the info to authentication detail when do authenticated, or you can use different Authentication instances, e.g UsernamePasswordAuthenticationToken for DB and LDAP(maybe need to create a new Authentication to separate them), SAMLAuthenticationToken for SAML.