Unable to make the oauth2Login as stateless - spring

I have provided the cookie based authorization request repository to oauth2Login() dsl to make it as stateless. but when I add the session creation policy as STATELESS , the oauth2 login is not working and returning "too many callbacks" error in UI page.
I have used the following oauth2Login config. for login with google oauth2 provider.
#Autowired
private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().csrfTokenRepository(new CookieCsrfTokenRepository())
.ignoringAntMatchers("/oauth2/authorization/google")
.and()
.sessionManagement(sessionMgmtConfig -> sessionMgmtConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated())
.oauth2Login(oauth2Config -> oauth2Config
.authorizationEndpoint(config -> config.authorizationRequestRepository(httpCookieOAuth2AuthorizationRequestRepository))
.userInfoEndpoint(config -> config.oidcUserService(oidcUserOAuth2UserService()))
.successHandler(authenticationSuccessHandler())
)
;//.logout(logoutConfig -> logoutConfig.addLogoutHandler(logoutHandler()))
}
public AuthenticationSuccessHandler authenticationSuccessHandler() {
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setRequestCache(new CookieRequestCache());
return successHandler;
}
If I comment out the session management line, it is working as expected and creating the JSESSION but when not working if I uncomment this part. Am I missing something?

It is not so easy to have oauth2Login and stateless sessionManagement.
The problem is that Spring needs to store information about OAuth 2.0 state parameter. Normally it is stored in session but when you disable it, Spring gets crazy ("too many callbacks") because it can not find it.
To solve that problem you can use your own cookie to store state parameter.
This can be done by providing a custom implementation of AuthorizationRequestRepository<OAuth2AuthorizationRequest>.
There is a nice blog post descriging everything in more details.
https://www.jessym.com/articles/stateless-oauth2-social-logins-with-spring-boot
What is the purpose of the 'state' parameter in OAuth authorization request

Related

Default 401 instead of redirecting for OAuth2 login Spring Security

I support two types of authentication and need to return 401 for most paths instead of redirects. For Keycloak I used the HttpUnauthorizedEntryPoint below and its fine, but for the OAuth2 login, it prevents the automatic redirect (on "/auth/challenge" in my case) to "/oauth2/authorization/azure" on NegatedRequestMatcher(/login, and some other things) to be put in place. The valid process is reflected in logs below:
org.springframework.security.web.util.matcher.NegatedRequestMatcher: matches = true
org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint: Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint#112c824c
org.springframework.security.web.context.SecurityContextPersistenceFilter: SecurityContextHolder now cleared, as request processing completed org.springframework.security.web.DefaultRedirectStrategy: Redirecting to 'http://localhost:2222/oauth2/authorization/azure'
This is the code that adds the Ouath2 bit to the common configuration:
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.commonConfiguration()
.exceptionHandling()
.authenticationEntryPoint(HttpUnauthorizedEntryPoint())
.and()
.oauth2Login()
.userInfoEndpoint()
.oidcUserService(userService)
}
public class HttpUnauthorizedEntryPoint implements AuthenticationEntryPoint {
private static final Logger LOG = LoggerFactory.getLogger(HttpUnauthorizedEntryPoint.class);
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException arg) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "This endpoint requires authorization.");
}
}
The question is, how can I by default return 401 and let all the OAuth2 redirects to be placed under the hood?
Thanks in advance.
I know that because of the HttpUnauthorizedEntryPoint the DelegatingAuthenticationEntryPoint is not filled in by Spring Security. I tried to add this manually, but I would rather have this process done by Spring.
val entryPoints = LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>()
entryPoints[loginPageMatcher] = LoginUrlAuthenticationEntryPoint("/oauth2/authorization/azure")
val loginEntryPoint = DelegatingAuthenticationEntryPoint(entryPoints)
loginEntryPoint.setDefaultEntryPoint(HttpUnauthorizedEntryPoint())
My guess is you serve both a REST API and server-side rendered UI (Thymeleaf, JSF or whatever) and you want to return
401 for unauthorized requests to #RestController
302 (redirect to login) for unauthorized requests to UI pages.
Define two SecurityFilterChain beans:
first with client configuration restricted to UI paths
#Order(Ordered.HIGHEST_PRECEDENCE)
http.securityMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/login/**"), new AntPathRequestMatcher("/oauth2/**"), ...); // add here all paths to UI resources
http.oauth2Login(); // and all client configuration you need for UI
http.authorizeHttpRequests().requestMatchers("/login/**").permitAll().requestMatchers("/oauth2/**").permitAll().anyRequest().authenticated();
second being default with resource-server configuration (no securityMatcher and an order greater than HIGHEST_PRECEDENCE)
Details in this answer and that tutorial

Can I use both introspection server and local check for authorize token? Spring Boot - Security

I want to
introspect JWT token on remote server
and then check locally if scope/aud/iss/exp are correct
How can this be done most easily in Spring Boot?
As I understand first case is something similar to opauqeToken functionality (but I have normal JWT) and second case is more like using jwt
Spring Security only supports JWTs or Opaque Tokens, not both at the same time.
If I use opaqueToken, then validation on remote server is done without any effort (even if that's JWT)
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/api/**").hasAuthority("SCOPE_" + scope)
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaque -> opaque
.introspectionUri(this.introspectionUri)
.introspectionClientCredentials(this.clientId, this.clientSecret)
));
return http.build();
I have scope verified. Now I want to check iss, aud, exp. Is that doable with opaqueToken?
Or should I use jwt auth instead?
IMHO opaqueToken can be JWT, so now the question is how to verify and inspect it locally after remote introspection?
It's kind of hybrid of two different approaches, but hopefully you know the simple way how to do it.
Ok, I think I have my answer. I created my own introspector which is implementing OpaqueTokenIntrospector
public class JwtOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector(
"introspect-url",
"client-id",
"client-secret"
);
#Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal introspected = this.delegate.introspect(token);
// SOME LOGIC
}
}
and I added it as a #Bean
#Bean
public OpaqueTokenIntrospector tokenIntrospector() {
return new JwtOpaqueTokenIntrospector();
}

Spring Boot OAuth2 errors after invalidating session and accessing data from Resource Server

My architecture consists of three applications:
Authorization Server (10.10.1.1:8080)
Client A (10.10.3.3:8084)
Client B (10.10.2.2:8089)
Both Client A and B are serving static Angular files. Both clients are "communicating" with each other - it is possible to navigate from the first one to another one and back as well (through normal window.location.replace).
Everything works great instead of one specific situation.
I'm logging into Client A application (through Authorization server redirect).
I'm opening Client B - user is properly authenticated based on Client A.
I'm coming back to Client A - user is still authenticated. (I can repeat steps 2 and 3 endlessly)
I'm logging out from Client A.
I'm logging in again into Client A with the same or different user.
I'm opening Client B and getting blank page due to some network issues.
After page refresh everything works fine (JSESSIONID is changing in the browser and user is properly authenticated).
I've tried couple different approaches and configurations using session invalidation. Session is properly invalidated but then it is not created again (user is changing to anonymous instead of being properly taken from Client A).
Then follows redirect to authorization server, which isn't available for some reason.
The problem here is that normal flow after redirect (step 2) is:
Redirect to 10.10.2.2:8089/home-page
/home-page gets 302 REDIRECT in network tab to /login
/login redirects to 10.10.1.1:8080/oauth/authorize
then it redirects back to /home-page with status 200 OK.
Error flow after redirect (step 6) is:
Redirect to 10.10.2.2:8089/home-page
/home-page gets 200 OK in network tab
application loads the page and it makes request for user data (/api/user) which gets 401
the entire redirect cycle takes place and ends with unability to redirect to 10.10.1.1:8080/oauth/authorize
after page refresh everything works fine.
I've tried:
couple different approaches and configurations in security (both on Client A and Client B)
allowing all origins in CorsFilter (for testing purposes - even that didn't help)
adding another cookie through server.servlet.cookie.name and erasing it by deleteCookies() or proper handler
adding maximumSessions(2) for tests purposes - even that didn't help
At last I made some tricky solution. I made request to Client B before redirect to Client A. It removed JSESSIONID through HttpServletResponse. It helped, but only when I'm working on one browser tab.
If I have two tabs opened (one with Client A and one with Client B) after doing step 5 and 6 and refreshing the page on Client B, problem still persists (because I didn't erase JSESSIONID from the browser).
I don't know if I understand this problem properly (that JSESSIONID is problematic in the browser), so correct me if I'm wrong. Also - I don't know how to erase this cookie or allow OAuth2 Filters to automatically create new one and invalidate the session in proper way.
Can anybody help me with this problem and show what I'm doing wrong here?
Client A - Security Configuration
#Configuration
#EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public OAuth2RestTemplate oauth2RestTemplate(final OAuth2ClientContext context,
final OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, context);
}
#Override
public void configure(final HttpSecurity http) throws Exception {
http
.formLogin()
.and()
.logout().clearAuthentication(true)
.invalidateHttpSession(true)
// .deleteCookies("JSESSIONID")
// .logoutSuccessHandler((httpServletRequest, httpServletResponse, authentication) -> {httpServletResponse.setStatus(HttpServletResponse.SC_OK);})
.addLogoutHandler(new ProperCookieClearingLogoutHandler("JSESSIONID"))
.and()
.authorizeRequests()
.antMatchers("/index.html", "/main.html", "/login", "/resources/**", ...)
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
Client B - Security Configuration
#Configuration
#EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public OAuth2RestTemplate oauth2RestTemplate(final OAuth2ClientContext context,
final OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, context);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.logout().clearAuthentication(true)
.invalidateHttpSession(true)
// .deleteCookies("JSESSIONID")
// .logoutSuccessHandler((httpServletRequest, httpServletResponse, authentication) -> {httpServletResponse.setStatus(HttpServletResponse.SC_OK);})
.addLogoutHandler(new ProperCookieClearingLogoutHandler("JSESSIONID"))
.and()
.authorizeRequests()
.antMatchers("/index.html", "/main.html", "/resources/**", "/login/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
Authorization Server - Security Configuration
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login")
.permitAll().and()
.formLogin().failureHandler(new FailureAuthenticationHandler())
.loginPage("/login").permitAll()
.and().requestMatchers()
.antMatchers("/login")
.antMatchers("/oauth/authorize")
.antMatchers("/oauth/confirm_access")
.and()
.anyRequest().authenticated()
.and().sessionManagement().maximumSessions(-1).expiredUrl("/...").sessionRegistry(sessionRegistry());
}
#Bean
SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
... authentication providers ...
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
CustomTokenStore customTokenStore;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("x").secret("x")
.authorizedGrantTypes("x").autoApprove(true).scopes("x");
}
#Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(customTokenStore).authenticationManager(authenticationManager);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.allowFormAuthenticationForClients();
}
}
Full log from Client B:
https://pastebin.com/aVc5AXcx
Thank you in advance.
24.10.2020 - TOPIC EDIT
After digging deeper and doing some research I probably found a core of the problem, but I don't know how to solve it yet.
First problem
There are two OAuth2 clients (annoted with #EnableOAuth2Sso) that share the same session and user data, but they don't know about each other openly (and about each other login/logout state).
I'm working on Client B and triggering logout call on that specific client.
Then I'm redirecting to authorization server login page with specific logout params.
I'm making logout call to authorization server on POST method and /logout path.
After successfull user logout I'm doing window.location.replace to Client A, which gets unauthorized error in network tab (401):
WWW-Authenticate header: Bearer realm="oauth2-resource", error="invalid_token", error_description="Invalid access token: 27ef8abe-e8e5-4d07-aaf4-a82a8757614e"
And in console of Client A I get:
UserInfoTokenServices: Could not fetch user details: class org.springframework.security.oauth2.client.resource.UserRedirectRequiredException, A redirect is required to get the users approval.
Second problem
Similiar situation is in the base problem stated in this topic. After relogin on Client A and page refresh on Client B, it has some session/token in cache and think that user is still authenticated in that client. It returns status 200 OK in HTML routing path (f.e. /home-page), but gets unauthorized on first request to API and giving the same invalid_token header:
WWW-Authenticate header: Bearer realm="oauth2-resource", error="invalid_token", error_description="Invalid access token: 4026cf9f-8081-4870-b9bf-6e6ff89d4ded" (401)
And in Resource Server I get:
Unable to obtain a new access token for resource 'null'. The provider manager is not configured to support it.
Both clients have the same configuration in properties
security.oauth2.client.client-id=x
security.oauth2.client.client-secret=y
security.oauth2.client.user-authorization-uri=http://${auhorization.server.url}/oauth/authorize
security.oauth2.client.access-token-uri=http://${auhorization.server.url}/oauth/token
security.oauth2.resource.user-info-uri=http://${resource.server.url}/user
Conclusion
I've tried adding csrfHeaderFilter and OAuth2ClientContextFilter from this topic, but it didn't help.
https://github.com/spring-guides/tut-spring-security-and-angular-js/issues/76
So the question is how to handle logout / refresh session and user context in another client after logout from the second one (and authorization server)? I don't know if I'm getting this process right, but I'm still anylizing what's going on here...
Can anybody show me some solution?

Spring Cloud Gateway Oauth2Login Return JWT Token Instead of SESSION Cookie Upon Successful Login

sorry in advance if the question is previously asked, but I have not been able to find an answer.
I am trying to setup Spring Cloud Gateway to act as a OAuth2 client to authenticate/login users via a Keycloak Authentication server. I have been able to achieve this using the following code snipet:
Security Config:
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfig {
private final GatewayAuthenticationSuccessHandler gatewayAuthenticationSuccessHandler;
public SecurityConfig(GatewayAuthenticationSuccessHandler gatewayAuthenticationSuccessHandler) {
this.gatewayAuthenticationSuccessHandler = gatewayAuthenticationSuccessHandler;
}
#Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http,
ReactiveClientRegistrationRepository clientRegistrationRepository) {
http
.authorizeExchange()
.pathMatchers("/ui/**").permitAll()
.anyExchange().authenticated()
.and()
.oauth2Login().authenticationSuccessHandler(gatewayAuthenticationSuccessHandler)
.and()
.oauth2ResourceServer().jwt();
http.logout(
logout ->
logout.logoutSuccessHandler(
new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
http.logout().logoutUrl("/logout");
http.csrf().disable();
http.httpBasic().disable();
http.formLogin().disable();
return http.build();
}
}
Auth Success Handler:
#Component
public class GatewayAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
#Value("${my.frontend_url}")
private String DEFAULT_LOGIN_SUCCESS_URL;
#Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
URI url = URI.create(DEFAULT_LOGIN_SUCCESS_URL);
return this.redirectStrategy.sendRedirect(webFilterExchange.getExchange(), url);
}
}
With this setup, the gateway app can authenticate the users and obtain a JWT token from the authentication server on behalf of the caller (UI app). Based on my understanding, Spring security then uses spring session to create and feed back a SESSION cookie to the caller. This session cookie can be used for subsequent calls to authenticate the user. The gateway would use the SESSION cookie value to retrieve the associated JWT token from the cache and relays it to the downstream resource servers when proxying requests. I have also setup a token refresh filter to refresh the JWT token on the caller's behalf and a Redis ache to share this session cookie between multiple instances of the gateway.
What I would like to do now is to return the actual JWT token that was retrieved by the gateway back to the caller (instead of a SESSION cookie). In other words I am hoping to make my gateway a little more stateless by using JWT end-to-end (instead of using SESSION cookie for caller --> gateway and then JWT for gateway --> resource servers). Is this even possible with the current state of spring cloud gateway?
PS. I am using spring boot version 2.2.8 and spring cloud version HOXTON.SR6
Not sure this can help , but try to add a SessionPolicy as STATELESS to your webfilter chain as shown below , and it should work.
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
Also you could try to override the sessionAuthenticationStrategy with a NullAuthenticatedSessionStrategy if you are extending your config class to WebSecurityConfigurerAdapter.
override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy {
return NullAuthenticatedSessionStrategy()
}

Restrict authentication method for endpoints with Spring Security

I want to secure a REST API. The rules are simple.
The user must call /api/authenticate to get a token
The user can use a token (received from /api/authenticate) to access the API /api/**
The endpoint /api/authenticate only accepts HTTP Basic authentication (no token authentication)
The endpoints /api/** (excluding /api/authenticate) only accepts token authentication (no Basic Authentication)
All remaining endpoints are public and doesn't require authentication.
I actually use this:
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private TokenAuthenticationProvider tokenAuthenticationProvider;
#Override
protected void configure(final HttpSecurity httpSecurity) throws Exception {
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.headers().disable();
httpSecurity.setSharedObject(TokenAuthenticationProvider.class, this.tokenAuthenticationProvider);
httpSecurity.antMatcher("/api/authenticate").httpBasic();
httpSecurity.antMatcher("/api/**").apply(new TokenAuthenticationConfigurer());
httpSecurity.authorizeRequests()
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll();
}
}
Actually, if I send a request with a token to /api/authenticate my configuration accepts the request. I think this happens because /api/authenticate is part of /api/**. So I need to exclude this path for token authentication.
How can I do that?
EDIT 1
If I use the .and() fluent style, the result is exactly the same.
#Override
protected void configure(final HttpSecurity httpSecurity) throws Exception {
httpSecurity.setSharedObject(TokenAuthenticationProvider.class, this.tokenAuthenticationProvider);
httpSecurity
.headers().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.antMatcher("/api/authenticate").httpBasic()
.and()
.antMatcher("/api/**").apply(new TokenAuthenticationConfigurer())
.and()
.authorizeRequests().antMatchers("/api/**").authenticated().anyRequest().permitAll();
}
EDIT 2
As I understand the SecurityBuilder (HttpSecurity), every call of antMatcher(...) in the configure(...) method overwrites the previous call. In the debug logs I can see, that Spring Security always tries to match the request path against /api/** but never agains /api/authenticate. If I switch the order, I can't access the API anymore, just /api/authenticate, because Spring Security now always tries to match agains /api/authenticate.
So the question is: How can I register multiple rules:
/api/authenticate -> HttpBasicConfigurer (.http())
/api/** -> TokenAuthenticationConfigurer (my token authentication configured, .apply(...))
Maybe it is because you always override the configuration of the parent and you do not use the and() method:
The Java Configuration equivalent of closing an XML tag is expressed using the and() method which allows us to continue configuring the parent. If you read the code it also makes sense. I want to configure authorized requests and configure form login and configure HTTP Basic authentication.
http://docs.spring.io/spring-security/site/docs/current/reference/html/jc.html#jc-httpsecurity

Resources