Using Spring Security 5 to integrate with OAuth2 secured RESTFull API without login and servlet context.

Howto config a Spring Security OAuth2 client that is capable of operating outside of the context of a HttpServletRequest,
e.g. in a scheduled/background thread and/or in the service-tier.

With the new Spring Security 5, there are a lot of examples about howto configure a client to access service like, Facebook, GitHub and many others with the standard OAuth2.
But today I found diffulties to get documentations about howto access OAuth2 secured RESTFull API with a RestTemplate client, without login and servlet context. And so I had to debug Spring Security framework to figure out the right configuration.

My env is as a follow:

Java 8
Spring Boot 2.4.1
Spring Security 5.4.2
Spring Web 5.3.2

The pom dependencies are:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

The api that I have to access are secured with OAuth2 with GRANT_TYPE=PASSWORD and client auth method equals to POST.
Every call to the Api must contains an Authorization header with the access token of type Bearer.
To obtain the access token, we need a token uri, a client id and the client username/password. All the information must be provided by the resource server.

What we need is a RestTemplateConfig. The file of this example can be found here.

We’re going to see the config step by step. First of all we need a ClientRegistrationRepository

    Builder b = ClientRegistration.withRegistrationId(registrationId);
    b.authorizationGrantType(AuthorizationGrantType.PASSWORD);
    b.clientAuthenticationMethod(ClientAuthenticationMethod.POST);
    b.tokenUri(tokenUri);
    b.clientId(clientId);
    ClientRegistrationRepository clients = new InMemoryClientRegistrationRepository(b.build());

the tokenUri end the clientId must be provided by the resource server.

Then we need the service:

    OAuth2AuthorizedClientService service = new InMemoryOAuth2AuthorizedClientService(clients);

the authorized client provider:

    OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder().password().refreshToken().build();

and the manager:

    AuthorizedClientServiceOAuth2AuthorizedClientManager manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(
            clients, service);
    manager.setAuthorizedClientProvider(authorizedClientProvider);
    manager.setContextAttributesMapper(new Function<OAuth2AuthorizeRequest, Map<String, Object>>() {

        @Override
        public Map<String, Object> apply(OAuth2AuthorizeRequest authorizeRequest) {
            Map<String, Object> contextAttributes = new HashMap<>();
            String scope = authorizeRequest.getAttribute(OAuth2ParameterNames.SCOPE);
            if (StringUtils.hasText(scope)) {
                contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME,
                        StringUtils.delimitedListToStringArray(scope, " "));
            }

            String username = authorizeRequest.getAttribute(OAuth2ParameterNames.USERNAME);
            if (StringUtils.hasText(username)) {
                contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
            }

            String password = authorizeRequest.getAttribute(OAuth2ParameterNames.PASSWORD);
            if (StringUtils.hasText(password)) {
                contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
            }

            return contextAttributes;
        }

    });

then we can add an interceptors to the RestTemplate:

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder, OAuth2AuthorizedClientManager manager) {
    RestTemplate restTemplate = builder.build();
    restTemplate.getInterceptors().add(new BearerTokenInterceptor(manager, username, password, registrationId));

    return restTemplate;
}


public class BearerTokenInterceptor implements ClientHttpRequestInterceptor {
    private final Logger LOG = LoggerFactory.getLogger(BearerTokenInterceptor.class);

    private OAuth2AuthorizedClientManager manager;
    private String username;
    private String password;
    private String registrationId;

    public BearerTokenInterceptor(OAuth2AuthorizedClientManager manager, String username, String password, String registrationId) {
        this.manager = manager;
        this.username = username; 
        this.password = password;
        this.registrationId = registrationId; 
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution)
            throws IOException {
        String accessToken = null;
        OAuth2AuthorizedClient client = manager.authorize(OAuth2AuthorizeRequest.withClientRegistrationId(registrationId)
                .attribute(OAuth2ParameterNames.USERNAME, username)
                .attribute(OAuth2ParameterNames.PASSWORD, password)
                .principal(principal).build()); 
        accessToken = client.getAccessToken() != null ? client.getAccessToken().getTokenValue() : null;
        if (accessToken != null) {
            LOG.debug("Request body: {}", new String(bytes, StandardCharsets.UTF_8));
            request.getHeaders().add("Authorization", "Bearer " + accessToken);
            return execution.execute(request, bytes);
        } else {
            throw new IllegalStateException("Can't access the API without an access token");
        }
    }

}

before every call, the manager try to authorize the client with the username and password provided by the resource server. If the authentication is successful, the server return a json like this:

{
“access_token”: “hjdhjYU00jjTYYT….”,
“token_type”: “Bearer”,
“expires_in”: “3600”,
“refresh_token”: “hdshTT55jhds…”,
}

Since the server support refresh token, we have configured the authorizedClientProvider to manage the refresh token in case the access token provided is expired.

That’s all!

2 thoughts on “Using Spring Security 5 to integrate with OAuth2 secured RESTFull API without login and servlet context.”

  1. You saved my life 🙂
    I need to implement exactly the use case you describe and have been banging my head against this for the last two days but wasn’t able to figure out how the pieces fit together correctly.

    Thank you so much!

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s