I want my application to be able to make REST API requests on behalf of the users of a certain platform. I've registered my app on the platform, and they have OAuth2 support with the following endpoints:
GET /login/oauth2/auth
, which is the authorization uri to start an authentication_code flowPOST /login/oauth2/token
, which is where I send the code (assuming the user consented) and get back a response that contains the access token and refresh token:{
"access_token": "...",
"refresh_token": "...",
"user": {"id":5, "name": "First Last"}
}
At first I used Spring Security's oauth2Login
:
spring:
security:
oauth2:
client:
provider:
the-provider:
authorization-uri: https://provider.domain.com/login/oauth2/auth
token-uri: https://provider.domain.com/login/oauth2/token
user-info-authentication-method: Bearer
user-info-uri: https://provider.domain.com/api/users/me
registration:
the-provider:
authorization-grant-type: authorization_code
client-id: my-client-id
client-secret: myClientS3cret
redirect-uri: "{baseUrl}/login/oauth2/code/the-provider"
@Bean
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
http.oauth2Login(Customizer.withDefaults());
http.authorizeHttpRequests(r -> r.anyRequest().authenticated());
return http.build();
}
Out of the box a user who accesses my app at "/"
is automatically is taken through the OAuth2 flow and Spring Security puts the user info into the security context. This is nice but I don't see how to get/use the access token to make subsequent API requests on behalf of the user.
The Authentication
is a OAuth2AuthenticationToken
whose principal
is a DefaultOAuth2User
whose authorities
are [ "OAUTH2_USER" ]
and whose attributes
are data from the user-info-uri
, but I don't see anywhere that would contain the access token and refresh token.
Doing a little more research, I realized maybe I should be using oauth2Client
instead of oauth2Login
but it's not entirely clear to me how that would work.
Just to clarify a bit of what I'm imagining, it would be something like
@Controller
public class MyController {
private final ReportService reportService;
public MyController(final ReportService reportService) {
this.reportService = reportService;
}
@GetMapping({"", "/"})
public String index(final Model model) {
model.addAttribute("report", reportService.generateReport());
return "index";
}
}
where the ReportService
makes a couple of REST API requests using the access_token
generated for the user, which I imagine it would get off of the security context authentication.
I guess my question is sort of geared towards is this something that I can get out of the box with Spring Security (via oauth2Login
or oauth2Client
, or would I need to build something custom with the Spring Security primitives?
oauth2Login
configures the app with authorization code and refresh token flows. So by conception, an app with oauth2Login
is an OAuth2 client.
An oauth2Client
is a Spring application that gets tokens from an authorization server. With Boot, OAuth2 clients are configured with spring.security.oauth2.client.*
properties. For oauth2Login
to work, those properties must expose at least one registration with authorization_code
.
Note that you can make about any kind of application (not only oauth2Login
, but also oauth2ResourceServer
, formLogin
, etc.) an OAuth2 client if it needs to authorize requests to a resource server.
In a Spring OAuth2 client application, you can get tokens from the (Reactive)OAuth2AuthorizedClientManager
.
@Service
public class ReportServiceImpl implements ReportService {
private final OAuth2AuthorizedClientManager clients;
private final OAuth2AuthorizeRequest req;
public ReportServiceImpl(OAuth2AuthorizedClientManager clients, @Value("${report-client-registration-id:the-provider}") String reportClientRegistrationId) {
super();
this.clients = clients;
this.req = OAuth2AuthorizeRequest.withClientRegistrationId(reportClientRegistrationId).build();
}
@Override
public ReportDto generateReport() {
final var authorized = clients.authorize(req);
final var bearerString = "Bearer %s".formatted(authorized.getAccessToken().getTokenValue());
// TODO: set the bearerString as Authorization header to the request using your favorite client;
return new ReportDto();
}
}
Note that most REST clients include features to authorize all requests transparently (get the token from an "authorized client" and set the Authorization header without cluttering the service code). For instance, RestClient
can be configured with requestInterceptor
functions, and WebClient
with filter
functions.
spring-addons-starter-rest
To avoid writing the code for the REST client and its authorization, I publish a Spring Boot starter. It integrates with Spring's HttpServiceProxyFactory
to add RestClient
(or WebClient
) auto-configuration for HTTP proxy and OAuth2 (or Basic) authorization.
Sample remote service description (probably generated from an OpenAPI spec using a tool like the openapi-generator-maven-plugin
):
@HttpExchange(accept = MediaType.APPLICATION_JSON_VALUE)
public interface ReportApi {
@GetExchange(url = "/report")
ReportDto generateReport();
}
spring-addons
dependency:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-rest</artifactId>
<version>8.0.0</version>
</dependency>
spring-addons
configuration to use a token from an authorized client (an alternate strategy is forwarding the Bearer which authorized the incoming request, but this is possible only on a resource server):
com:
c4-soft:
springaddons:
rest:
client:
# By default in a servlet, exposes a RestClient bean named reportClient
report-client:
base-url: http://reporting-service
authorization:
oauth2:
# Kept your registration ID, even if it is rather confusing
oauth2-registration-id: the-provider
Having the remote service client generated:
@Configuration
public class RestConfiguration {
@Bean
// Inject the reportClient bean auto-configured from properties
ReportApi reportApi(RestClient reportClient) {
return new RestClientHttpExchangeProxyFactoryBean<>(ReportApi .class, reportClient).getObject();
}
}
Usage in a Spring component:
@Controller
@RequiredArgsConstructor
public class SomeController {
// Abracadabra! This is successfully auto-wired
private final ReportApi reportApi;
...
}
Note that there is absolutely no code for ReportApi
implementation or authorization, all is generated!
In my projects, even the client interfaces (those decorated with @HttpExchange
like the ReportApi
above) are generated from the OpenAPI spec, itself generated by Swagger from the remote service @RestController
sources (decorated with @RequestMapping
variants).