I try to run all 3 oAuth components of oAuth using Spring Boot 3.4.1. I used the oauth-server-docs and client + resource-server docs to implement.
The oAuth client can have 2 modes:
I want to use both, which is possible according to the docs.
My code - all components - is on GitHub: https://github.com/OhadR/oAuth2-sample
All seems to work fine except one thing: when the client-app tries to call the resource-server before the user has logged in, the resource-server return 401 (which is ok, i think) but the client-app, rather than redirecting to the oauth-server, just return 500.
I have tried to debug Spring, for no avail.
I see that in OAuth2AuthorizationRequestRedirectFilter.doFilterInternal()
, when exception is thrown from the rest of the filter-chain, Spring checks whether it is ClientAuthorizationRequiredException
and if so it sendRedirectForAuthorization()
. But in my case from some reason this code never runs (I put a breakpoint there and it never stops there).
Also, I see that AuthorizationCodeOAuth2AuthorizedClientProvider.authorize()
, which is the one that suppose to throw the ClientAuthorizationRequiredException
, is not called. (It is called only in "login" flow, when client-app calls the resource-server after user already logged in).
what does work? mode (1) above, meaning the client-app has a "login" button, which calls /login/oauth2/code/{registrationId}. This redirects to the oauth-server, the user logs-in, and then when the client-app tries to call the resource-server, it works fine, it gets 200 with response.
But mode (2) is not working. What am I missing?
Thanks!
Let's call what sends the request F
, the application in the middle G
and the resource server at the end RS
.
As RS
is configured as an OAuth2 resource server (oauth2ResourceServer
in Spring Security DSL), it expects requests to be authorized with a Bearer
token issued by an authorization server it trusts (or one of the authorization servers it trusts in case of multitenancy).
Access tokens are issued to OAuth2 clients, which can be either
G
F
if G
is configured to forward the access token it receives from F
There are two main OAuth2 flows to get an access token (both F
and G
can use either of it):
RS
trusts the OAuth2 client (either F
or G
) that it knows what it does with the resource and did upfront verifications if an end-user is involvedSpring Security gives complete flexibility for implementing the various options from above. A few things to consider however:
F
runs on end-user device (React, Vue, Angular, mobile apps, ...), it is now recommended that it is not configured as an OAuth2 client. Next apps using the next-auth
lib are not concerned because the OAuth2 client is running on the server side of the app (a Node instance). It can be configured as confidential clients and keep the tokens safe on the server.G
might be an OAuth2 BFF: a middleware on the backend that bridges between session-based authorization for apps running on end-user devices, and token-based authorization for downstream resource servers. I wrote a tutorial on Baeldung to configure Spring Cloud Gateway as an OAuth2 BFF: with oauth2Login
(authorization code and refresh token flows) and the TokenRelay=
filter (replaces session cookies with the access token in session when routing requests)G
might use something different than OAuth2 to authenticate users (formLogin
?) and use an OAuth2 client registration
with client credentials to authorize its requests to RS
F
is an oauth2 client, the request received by G
can be authorized with an access token. If it is desired that G
authorizes the request it sends with the token it received, then there is no need for OAuth2 client configuration on G
: the token is taken from the security context (this is something I use frequently for inter resource servers communication in micro-service architecture)G
is configured with two different registration
entries in application properties with:
authorization_code
to authenticate usersclient_credentials
to authorize its request to RS
On G
, RestClient
and WebClient
requests authorization is not directly tied to the state of the session and we should provide respectively ClientHttpRequestInterceptor
or ExchangeFilterFunction
to set the authorization header with a Bearer
token.
For now, Spring Security provides OAuth2ClientHttpRequestInterceptor
and ServerOAuth2AuthorizedClientExchangeFilterFunction
(ServletOAuth2AuthorizedClientExchangeFilterFunction
when using WebClient
in a servlet). Those set the authorization header with a token obtained using a client registration
(can be an authorization code registration in an app with oauth2Login
or a registration with client credentials in any kind of app).
When using a registration with authorization code, this functions expect the session to be authorized (the user must be logged-in already). So, endpoints delegating some of their processing to RS
must be protected with isAuthenticated()
or the internal request will be answered with a 401
.
The 302 Redirect to login
happens on applications with oauth2Login
only if the endpoint receiving a request is configured with isAuthenticated()
(or requires any authority). The requests sent by a REST client internal to this application are not concerned (the case of the request interceptors / filter functions referenced above and for which you are pulling your hair).
If wanting to forward the access token in the security context, we need to write a ClientHttpRequestInterceptor
or ExchangeFilterFunction
ourselves (nothing complicated).
I created a Spring Boot starter to auto-configure RestClient
or WebClient
beans with OAuth2 authorization (and HTTP proxy if needed) using just application properties. You should give it an eye, it might ease your life, whatever option you choose.
G
is a Boot app with Thymeleaf and oauth2Login
I use here:
@HttpExchange
service proxy factory generation, which is a recent feature of Spring Web (the implementation is generated for us from an interface definition and a RestClient
or WebClient
instance)RestClient
or WebClient
beans using application properties<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-rest</artifactId>
</dependency>
spring:
security:
oauth2:
client:
provider:
sso:
issuer-uri: ${issuer}
registration:
messaging-client-oidc:
provider: sso
authorization-grant-type: authorization_code
client-id: messaging-oauth2-client
client-secret: change-me
scope: openid
com:
c4-soft:
springaddons:
rest:
client:
message-client:
base-url: ${messages.backend-base-uri}
authorization:
oauth2:
oauth2-registration-id: messaging-client-oidc
// This can be generated from the OpenAPI spec of the remote service
@HttpExchange
public interface MessageApi {
@GetExchange(value = "/messages")
public List<String> getMessages();
}
@Configuration
public class RestConfiguration {
// messageClient @Bean is auto-configured by spring-addons using the properties above
// messageApi @Bean is a generated proxy implementing the MessageApi interface above
@Bean
MessageApi messageApi(RestClient messageClient) throws Exception {
return new RestClientHttpExchangeProxyFactoryBean<>(MessageApi.class, mesageClient).getObject();
}
}
@Controller
@RequiredArgsConstructor
public class MessagesController {
private final MessageApi messageApi;
@GetMapping(value = "/messages-restclient-attrs")
public String messagesUsingRestClientWithAttributes(Model model) {
model.addAttribute("messages", messageApi.getMessages());
return "index";
}
}
I skipped the security configuration with oauth2Login
which can be rather cumbersome when needing to map Spring authorities from nested private claims (case of Keycloak and Auth0 for instance). See this other starter of mine to do it using just application properties.
If G
was an OAuth2 BFF, it would have a similar registration
in properties, but would probably not declare and use REST client itself. However, the TokenRelay
filter would work mostly like the messageClient
above (using tokens from a registration for authorization-code & refresh-token flows).
If G
was a resource server, it would be configured either with a client-credentials registration
, or to forward the Bearer in the security context (maybe obtained in the first place by F
using authorization-code flow). The messageClient
and messageApi
beans would be created the same way (only the application properties would change).