How to handle OAuth2 access token refresh with synchronous API calls, in Spring Security 5 - spring-boot

We are using Spring Gateway (Spring Boot 2.4.6) which uses Spring Security 5 and the Weblux/ reactive model within that to provide OAuth2 security and Keycloak as the IDP.
Refreshing of the Access Token is an issue when our front-end application, which has already [successfully] authenticated against the gateway/ IDP, issues multiple API calls after the session's access token has expired.
It appears that out of (for example) five API calls, only the last one gets re-authenticated against the Keycloak provider and the other four get 'lost', thereby causing issues within the front-end.
If the user refreshes the UI's page then the proper authentication flow happens seamlessly and the token stored in the session is refreshed, without a redirect to the Keycloak login screen (as expected), therefore the problem is only with synchronous API calls.
The SecurityWebFilterChain is setup with:
/*
* Enable oauth2 authentication on all requests, but use our custom
* RegistrationRepository
*/
.and()
.oauth2Login()
.authenticationSuccessHandler(new AuthSuccessHandler(requestCache)) // handle success login
.authenticationFailureHandler((exchange, excep) -> {
LOGGER.debug("Authentication failure: {}", excep.getMessage());
return Mono.empty();
})
.clientRegistrationRepository(clientReg);
// Add our custom filter to the security chain
final KeycloakClientLoginFilter keyclockLogin = new KeycloakClientLoginFilter(
clientReg,
redirectStrategy,
requestCache,
authClientService);
clientReg.setKeycloakClientLoginFilter(keyclockLogin);
http.addFilterBefore(keyclockLogin, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
return http.build();
With the ServerAuthenticationSuccessHandler configured with this:
private class AuthSuccessHandler implements ServerAuthenticationSuccessHandler {
private final ServerRequestCache requestCache;
private final URI defaultLocation = URI.create("/login");
private AuthSuccessHandler(ServerRequestCache requestCache) {
this.requestCache = requestCache;
}
#Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
final ServerWebExchange exchange = webFilterExchange.getExchange();
return requestCache.getRedirectUri(exchange)
.defaultIfEmpty(defaultLocation)
.flatMap(location -> {
LOGGER.debug("Authentication success. Redirecting request to {}", location.toASCIIString());
return redirectStrategy.sendRedirect(exchange, location);
});
}
}
Within the KeycloakClientLoginFilter there is a ServerWebExchangeMatcher that checks if the required details are present on the inbound exchange, and whether the AccessToken has (or is about to) expire. If it is, it runs through this code to redirect the request off to Keycloak for authentication and/ or refresh:
final ClientRegistration keycloakReg = clientReg.getRegistration(tenantId, appId);
if (!isError && loginRedirects.containsKey(keycloakReg.getRegistrationId())) {
final String contextPath = exchange.getRequest().getPath().contextPath().value();
final URI redirect = URI.create(contextPath + loginRedirects.get(keycloakReg.getRegistrationId()));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("About to redirect to keycloak; for method {}, tenant={}",
exchange.getRequest().getMethod(),
tenantId);
}
// Save the request so the URL can be retreived on successful login
return requestCache.saveRequest(exchange)
.then(redirectStrategy.sendRedirect(exchange, redirect));
}
So, all API calls hit the above code, require a refresh, have their original exchanges saved in the requestCache and are then directed to Keycloak. When Keycloak responds with the updated token, the exchange(s) run through the AuthSuccessHandler, which pulls the original request URL from the requestCache and redirects the call to that original URL.
This part works for web requests and the one in five API calls.
The other four API calls never make it to the AuthSuccessHandler - They simply get 'lost'.
There are some ugly hacks that could be done, like blocking all calls until the one first one is re-authenticated, but that just isn't right and would be hard to get right anyway.
So can the gateway, CookieServerRequestCache or AuthenticationWebFilter only handle one request at a time? Is there a 'simple' implementation of waiting on one call from the same session to re-authenticate?
Any help would be greatly appreciated as the application simply doesn't work (from a user's perspective) until this is resolved.

I know quite some tutorials do so, but in my opinion, authenticating against the gateway is a mistake (see this answer for details why). Why not using an OAuth2 client library on your client(s) instead?
I personnaly use angular-auth-oidc-client, and I am convinced that there must be equivalents for React, Vue, Flutter, Android or iOS.
Such libraries can handle access-tokens refreshing for you (provided that you requested the offline_access scope and that the authorization-server supports refresh-token for your client).
Authenticate users on the client(s) with the help of a certified lib, have your gateway just forward Authorization header and configure your micro-services as resource-servers.

Related

How to specify custom return Url after receiving the token or on failure?

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);
}

Reactive Spring: How to pass Access Token from OAuth 2.0 Authorization Code Flow to scheduled task

I'm trying to build an application that periodically fetches data from a Third-Party API that demands a reCAPTCHA protected OAuth 2.0 Authorization Code Flow with PKCE for authentication.
I guess, it wouldn't be a big deal to implement the authorization protocol manually but I'm willing to do that using the Spring Security OAuth Client in the reactive manner.
The goal is to have a scheduled task that fetches the data from the API only being blocked until I manually open up a login page (currently a REST endpoint) in the browser that forwards me to the login page of the API vendor. After successful authentication, the scheduled task should also be able to access the API.
Currently the class structure looks like this:
MyController#showData and MyScheduler#fetchData both call ApiClient#retrieveData which does the final API call using the reactive WebClient from Spring.
The WebClient configuration looks like this:
#Configuration
#EnableWebFluxSecurity
class WebClientConfiguration {
#Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegs,
ReactiveOAuth2AuthorizedClientService authClientService) {
ReactiveOAuth2AuthorizedClientManager authClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegs, authClientService);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authClientManager);
oauth.setDefaultOAuth2AuthorizedClient(true);
oauth.setDefaultClientRegistrationId("test");
return WebClient.builder()
.filter(oauth)
.build();
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ServerOAuth2AuthorizationRequestResolver resolver) {
http.authorizeExchange()
.anyExchange()
.authenticated()
.and()
.oauth2Login(auth -> auth.authorizationRequestResolver(resolver));
return http.build();
}
#Bean
public ServerOAuth2AuthorizationRequestResolver pkceResolver(
ReactiveClientRegistrationRepository repo) {
DefaultServerOAuth2AuthorizationRequestResolver resolver =
new DefaultServerOAuth2AuthorizationRequestResolver(repo);
resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
return resolver;
}
}
The authorization works fine. When I open /showData in the browser, I'm redirected to the vendor's login page and when I come back, the requested data is displayed as it should be.
But the Scheduler is still blocked. I guess that has something to do with the Security Context which is linked only to the browser session, but I'm not so familiar with Spring Security to understand how to share the access (and refresh) token within the whole application.
Disclaimer: The Third-Party API has specific endpoints which are explicitly meant to be called periodically and not only on a user's request, but they still demand authorization by Authorization Code instead of Client Credential.
If you want to call third-party API from the scheduler you should pass OAuth2AuthorizedClient to it. That class represent an OAuth2 authorized client and has information about access/refresh token.
Here is the documentation describing how to use it.

Spring Boot Authorization Server + Google OAuth2/OpenId Connect should work with access_token or id_token?

I'm a bit confused regarding whether I should be accessing my Spring Boot Resource Server via an access_token or an id_token.
First, let me quickly explain my setup:
Spring Boot app as an OAuth 2.0 Resource Server. This is configured as described in the Spring docs: Minimal Configuration for JWTs This app provides secured #Controllers that will provide data for a JavaScript SPA (eg. React)
Google's OAuth 2.0 AP / OpenID Connect already configured (Credentials, Client Id, Client Secret)
A JavaScript SPA app (eg. React) that logs the user into Google and makes requests to the Spring Boot Resource Server for secured data. These requests include the Authorization header (with Bearer token obtained from Google) for the logged in user.
For development purposes, I'm also using Postman to make requests to the Spring Boot Resource Server
I can easily configure Postman to get a token from Google. This token response from Google includes values for access_token, id_token, scope, expries_in and token_type.
However, my requests to the Resource Server are denied when Postman tries to use the value from retrieved token's access_token field as the Bearer in the Authorization header
The only way I'm able to successfully access the secured #Controllers is by using the id_token as the Bearer in the Authorization header.
Is it expected that I should use the id_token as the Bearer in the Authorization header? Or is it expected that I should use the access_token?
Some additional relevant info:
The value of the id_token is a JWT token. The value of the access_token is not a JWT token. I know this because I can decode the id_token on jwt.io but it is unable to decode the value of the access_token. Further, the Spring Boot Resource Server fails with the following when I send the access_token as the Bearer in the Authorization header:
An error occurred while attempting to decode the Jwt: Invalid unsecured/JWS/JWE header: Invalid JSON: Unexpected token ɭ� at position 2.
This blog post Understanding identity tokens says the following:
You should not use an identity token to authorize access to an API.
To access an API, you should be using OAuth’s access tokens, which are intended only for the protected resource (API) and come with scoping built-in.
Looking at at the spring-security-samples for using OAuth2 Resource Server, I see the value of there hard-coded access_token (for testing purposes) is indeed a valid JWT. As opposed to the access_token returned from Google which is not a JWT.
In summary:
I can access my Spring Boot Resource Server using the value of the id_token obtained from Google. The value of the access_token is not a JWT and fails to parse by Spring Boot.
Is there something wrong with my understanding, my configuration or what? Does Google's OpenId Connect behave differently regarding how the access_token works?
Happy to clarify or add more info if needed. Thanks for your consideration and your patience!
The blog post you mentioned is correct in my view, and I believe the OpenID Connect 1.0 spec does not intend for an id_token to be used for access purposes.
Like you, I expected that using Google as an Authorization Server would work out of the box, because Spring Security works with Google as a common OAuth2 provider for providing social login. However, this is not the case, and I believe it is not really intended, because Google is not really your authorization server. For example, I don't believe you can configure Google to work with scopes/permissions/authorities of your domain-specific application. This is different from something like Okta, where there are many options for configuring things in your own tenant.
I would actually recommend checking out Spring Authorization Server, and configuring Google as a federated identity provider. I'm working on a sample for this currently and it will be published within the next week or so (see this branch).
Having said that, if you're still interested in a simple use case where Google access tokens are used for authenticating with your resource server, you would need to provide your own opaque token introspector that uses Google's tokeninfo endpoint. It doesn't match what Spring Security expects, so it's a bit involved.
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// #formatter:off
http
.authorizeRequests((authorizeRequests) -> authorizeRequests
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
// #formatter:on
return http.build();
}
#Bean
public OpaqueTokenIntrospector introspector() {
return new GoogleTokenIntrospector("https://oauth2.googleapis.com/tokeninfo");
}
}
public final class GoogleTokenIntrospector implements OpaqueTokenIntrospector {
private final RestTemplate restTemplate = new RestTemplate();
private final String introspectionUri;
public GoogleTokenIntrospector(String introspectionUri) {
this.introspectionUri = introspectionUri;
}
#Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
RequestEntity<?> requestEntity = buildRequest(token);
try {
ResponseEntity<Map<String, Object>> responseEntity = this.restTemplate.exchange(requestEntity, new ParameterizedTypeReference<>() {});
// TODO: Create and return OAuth2IntrospectionAuthenticatedPrincipal based on response...
} catch (Exception ex) {
throw new BadOpaqueTokenException(ex.getMessage(), ex);
}
}
private RequestEntity<?> buildRequest(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("access_token", token);
return new RequestEntity<>(body, headers, HttpMethod.POST, URI.create(introspectionUri));
}
}
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://accounts.google.com
jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs

GraphQL Subscriptions - Spring Boot Websocket Authentication

We are using the Netflix DGS framework to build our backend to provide a GraphQL API.
In addition to that we use Keykloak as an identity provider which comes with a handy Spring module to add support for authentication and authorization out of the box.
Every request contains a JWT token, which gets validated and from there a SecurityContext object is being generated which is then available in every endpoint.
This is working great for HTTP requests. GraphQL queries and mutations are sent via HTTP, therefore no problem here.
Subscriptions on the other hand use the web socket protocol. A WS request does not contain additional headers, therefore no JWT token is sent with the request.
We can add the token via a payload, the question is now how to set up a Spring Security Filter which creates a Security Context out of the payload.
I guess this is rather Spring specific, basically a filter which intercepts any web socket request (ws://... or wss://...) is needed.
Any help or hint is very much appreciated!
The only way to use headers in web socket messages is in the connection_init message. the headers will be sent by the client in the payload of the message.
The solution I propose is done in 2 steps (We will assume that the name of the header element is "token"):
Intercept the connection_init message, then force the insertion of a new element (token) in the subscription request.
Retrieve the element (token) of the header during the interception of the subscription and feed the context.
Concretely, the solution is the implementation of WebSocketGraphQlInterceptor interface
#Configuration
class SubscriptionInterceptor implements WebSocketGraphQlInterceptor {
#Override
public Mono<Object> handleConnectionInitialization(WebSocketSessionInfo sessionInfo, Map<String, Object> connectionInitPayload) {
sessionInfo.getHeaders().add("token", connectionInitPayload.get("token").toString());
return Mono.just(connectionInitPayload);
}
#Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
List<String> token = request.getHeaders().getOrEmpty("token");
return chain.next(request).contextWrite(context -> context. Put("token", token.isEmpty() ? "" : token.get(0)));
}
}

Spring Authorization Server: How to use login form hosted on a separate application?

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

Resources