I’ve seen a couple of posts talking about something similar, though not exactly like what I’m trying to do. Namely I came across https://www.jessym.com/articles/stateless-oauth2-social-logins-with-spring-boot which is quite a complicated guide to still fit within Spring Security, and have what the author refers to as "stateless oauth2 logins". The issue with their approach is that it still relies on cookies, which I would rather avoid, as they are still vulnerable to things like CSRF attacks.
Here I’ve provided a graphic of what I’m trying to accomplish.
In the first section is what I’ve observed Spring Security’s default behavior to be ( I’m on Spring Security 6.2.0 ) with the following configuration and properties:
SecurityConfiguration.java:
@Configuration
public class SecurityConfiguration {
private static final String[] SWAGGER_URLS = {
"/v3/api-docs/**",
"/swagger-ui/**",
"/v2/api-docs/**",
"/swagger-resources/**"
};
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, ClientRegistrationRepository clientRegistrationRepository) throws Exception {
String base_uri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
DefaultOAuth2AuthorizationRequestResolver resolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, base_uri);
resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
return httpSecurity
.authorizeHttpRequests(c -> {
// permit anyone to get to error pages
c.dispatcherTypeMatchers(DispatcherType.ERROR).permitAll();
// permit all swagger urls
c.requestMatchers(SWAGGER_URLS).permitAll();
// require authentication for all other cases
c.anyRequest().authenticated();
})
.oauth2Login(c -> {
c.authorizationEndpoint(auth -> auth.authorizationRequestResolver(resolver)); // use PKCE
})
.build();
}
// default SpringHttpFirewall is a bit too strict
@Bean
public HttpFirewall getHttpFirewall() {
StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
strictHttpFirewall.setAllowUrlEncodedDoubleSlash(true);
return strictHttpFirewall;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
(relevant) application.properties:
# GitHub OAuth2
spring.security.oauth2.client.registration.github.client-id=${GITHUB_CLIENT_ID}
spring.security.oauth2.client.registration.github.client-secret=${GITHUB_CLIENT_SECRET}
spring.security.oauth2.client.registration.github.scope=user:read
So, by default, I have observed that Spring will go through the whole OAuth2 authorization scheme, and then in the response to the callback ( redirect uri ), it will add a Set-Cookie
with a JSESSIONID in it. This is stateful session management, and it stores all the data relating to the user in a Map or something in memory by default against the provided jsessionid.
I don’t like this for 2 reasons:
-
The sessions stored in memory will of course be wiped if the application shuts down for any reason. This can be remedied by storing session information in a database or redis, but then that’s an extra database request on every single authorized HTTP request to fetch the relevant information about the session.
-
Cookie-based authentication/authorization is in general vulnerable to CSRF attacks, since all cookies are sent along with whatever request is being made for the domain the cookie is stored for. This doesn’t change when https is involved, the cookies are just encrypted in that case, but they’re sent all the same. Having something like an Authorization header-based scheme means that any random request to an API won’t just be magically authenticated, it requires some javascript in the front-end to actually attach that header.
I want to address both of these by using JWT tokens provided in an Authorization header, instead of a JSESSIONID. The way I want to do this is, in the response from the callback from my app, I would like to provide the header along with a JWT token that I generate after I send the authorization code to the auth service and get back an access token. Note: the JWT I provide to the browser will not be the same as the access token I get from the auth service. That will be stored somewhere ( database, redis, w/e ) for later use, if necessary.
That about covers what I’m trying to accomplish. Please, if anyone’s done this before, or has relevant information/articles/documentation on how to get it done, I would appreciate it greatly. Thank you.
2
Answers
What you want to achieve is less secured than session cookie, with CSRF protection: session cookies are not accessible to Javascript (your JWT will have to be) and you’ll send not only access token but also refresh token with almost every request (both are contained in your JWT). Things as sensible as a refresh token are better kept in your server memory than in your users browsers Javascript code (and exchanged as less frequently as possible).
A stateful gateway is not evil when all it keeps in memory is OAuth2 tokens: if the instance is lost, the authorization_code flows to re-authenticate users still having a valid session on the authorization server (yes, it is a session), will happen silently (just a few redirects, no prompt).
If you have such a high traffic that a single spring-cloud-gateway is not enough, spring-session is there for you. This addresses the need for a session during the flow, but also improves the recovering when switching gateway instances (sessions are not lost).
I my opinion, a stateful gateway configured as OAuth2 client (with sessions and CSRF protection) to store tokens, the
TokenRekay
filter to replace on the fly this cookie withBearer Authorization
header, and downstream services configured as stateless resource servers, is just fine. Considering that I can find session cookies when inspecting about any major website with my browser debugging tools, it must be acceptable to the security experts of companies absorbing daily more traffic than you will probably ever do in a year…Just to add to the other good answers some additional information.
you are worried about CSRF attacks, and that a cookie will get sent with every request. That’s why cookies have a flag called
SameSite
to try to prevent such attacks.By using a JWT in the way you want you instead open up to a completely different flora of other types of attacks.
Some vulnerabilities when using JWTs as sessions:
The latest OAuth 2.0 Security Best Current Practice draft from ietf posted on 23 October 2023 deprecates the implicit grant flow which is the flow that issues tokens straight out to clients, because there are several redirect and referral vulnerabilities that can be utilized by malicious actors to trick users to login and pass the token to the malicious actor instead.
JWT is a data format to sign data. That’s what it is, nothing more, nothing less. They weren’t designed to replace sessions. During all the the years, cookies have existed they have been exploited in different ways and like a cat and mouse game the world has updated them to protect from newer and newer types of attacks. The flags,
Secured
,httpOnly
andSameSite
are all constructs because internet has learnt, iterated, and become more secure.These are features that don’t exist in JWT’s, so by using JWTs as sessions we are basically taking a 15 year leap back in time to an internet where credential stealing using XSS was a thing.
The only place JWTs have a role in modern security is for server to server communication, between either a BFF and an issuer (google, facebook), where the server keeps the JWT and issues cookies to the browser.
Or server to server communication, for one-shot requests. Basically "here is a request, this is the token that proves who i am for 1 minute". Like in Kerberos with tickets.
If you want to build scalable enterprise security, build proper oauth2 security using the Open ID Connect standard. Thats the best we have currently and for the love of god, DONT BUILD CUSTOM HOME MADE SECURITY there is always someone smarter than you that will break it.
I dont want to go back to the old internet please, we are better than that.