Everyone thought this first time when they set Spring Cloud Gateway to use oauth2. Where is my access token?
- You check browser’s network tab and there is no access token on request headers. There is only session on request headers.
- You check resource server’s incoming logs and there is access token on request headers.
Really, where’s the access token? It must be on Spring Cloud Gateway, but where?
TokenRelay
The answer is TokenRelay filter. If you check TokenRelayGatewayFilterFactory class, there is apply() method. Let’s deep dive into.
- It uses authorizedClientRepository to load authorized clients.
- There are 3 implementation of this repository , and it uses AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository if you are logged in.
- authorizedClientRepository also uses authorizedClientService to find client.
- There is only 1 implementation of this service and it is InMemoryReactiveOAuth2AuthorizedClientService.
Here is what we are looking for. This class has ConcurrentHashMap and stores sessions as key, access tokens as value.
private final Map<OAuth2AuthorizedClientId, OAuth2AuthorizedClient> authorizedClients = new ConcurrentHashMap<>();
So, when request comes into Spring Cloud Gateway, TokenRelay filter takes session from request and finds access token corresponding session.
High Availability
Let’s say that you are 3 instances (i1, i2, i3) of Spring Cloud Gateway. You will store sessions on Redis. But, sharing session information between all instances does not solve our problem. Access tokens are still on instances, because they are stored as in-memory. There is no out-of-box solution for this.
For example, your first request comes into i1 instance and authentication process continued over i1 instance. So, the session will go Redis, access token will go i1’s ConcurrentHashMap. When your next requests come into i2 instance, the session will be retrieved but not found corresponding access token. Because it is searched at i2’s ConcurrentHashMap.
Here is the related documentation part of this. (https://cloud.spring.io/spring-cloud-static/Greenwich.SR5/multi/multi__more_detail.html)
The default implementation of ReactiveOAuth2AuthorizedClientService used by TokenRelayGatewayFilterFactory uses an in-memory data store. You will need to provide your own implementation ReactiveOAuth2AuthorizedClientService if you need a more robust solution.
The solution is to create new ReactiveOAuth2AuthorizedClientService class and set it to primary bean.
@Component
@Primary
public class AccessTokenRedisConfiguration implements ReactiveOAuth2AuthorizedClientService {
@Override
@SuppressWarnings("unchecked")
public <T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId, String principalName) {
// get them from redis
// key: <principalName>, hkey: accessToken
}
@Override
public Mono<Void> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
// put them to redis as hash
// key: <principalName>, hkey: accessToken, hvalue: <authorizedClient>
}
@Override
public Mono<Void> removeAuthorizedClient(String clientRegistrationId, String principalName) {
// delete them from redis
// key: <principalName>
}
}
Just write SessionService bean that uses ReactiveRedisTemplate to put/get/delete operations on Redis. Then use this service in AccessTokenRedisConfiguration.
There is another solution without creating new ReactiveOAuth2AuthorizedClientService. Use reverse proxy in front of Spring Cloud Gateway and take advantage of Sticky Sessions feature. So, your requests will always go to the server where you are logged in to get valid access token.
- If you use NGINX, then use ip_hash in upstream.
- If you use Kubernetes service object, then use ClientIP in session affinity.