Вот что я узнал после дальнейшего расследования.Код, описанный в моем вопросе, никогда не собирался вызывать client_credentials и соответствовать моему сценарию использования.Я думаю (не уверен на 100%), что это будет очень полезно в будущем, если я попытаюсь распространить представленный пользователем токен по нескольким сервисам в архитектуре микросервисов.Вспоминается цепочка подобных действий:
Пользователь вызывает Сервис A -> Сервис A вызывает Сервис B -> Сервис B отвечает -> Сервис A отвечает обратно на запрос пользователя.
И используя один и тот же токен для начала всего процесса.
Мое решение моего варианта использования:
Я создалновый класс Filter в значительной степени основан на оригинале и реализует шаг перед выполнением запроса, где я проверяю, сохранен ли у меня токен JWT, который можно использовать для API управления Auth0.Если я не создаю запрос на получение client_credentials и получаю его, то присоединяю этот токен в качестве носителя к начальному запросу и выполняю его.Я также добавил механизм кэширования в памяти небольшого токена, чтобы, если токен действителен, любые другие запросы позже использовали его.Вот мой код.
Фильтр
public class Auth0ClientCredentialsGrantFilterFunction implements ExchangeFilterFunction {
private ReactiveClientRegistrationRepository clientRegistrationRepository;
/**
* Required by auth0 when requesting a client credentials token
*/
private String audience;
private String clientRegistrationId;
private Auth0InMemoryAccessTokenStore auth0InMemoryAccessTokenStore;
public Auth0ClientCredentialsGrantFilterFunction(ReactiveClientRegistrationRepository clientRegistrationRepository,
String clientRegistrationId,
String audience) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.audience = audience;
this.clientRegistrationId = clientRegistrationId;
this.auth0InMemoryAccessTokenStore = new Auth0InMemoryAccessTokenStore();
}
public void setAuth0InMemoryAccessTokenStore(Auth0InMemoryAccessTokenStore auth0InMemoryAccessTokenStore) {
this.auth0InMemoryAccessTokenStore = auth0InMemoryAccessTokenStore;
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return auth0ClientCredentialsToken(next)
.map(token -> bearer(request, token.getTokenValue()))
.flatMap(next::exchange)
.switchIfEmpty(next.exchange(request));
}
private Mono<OAuth2AccessToken> auth0ClientCredentialsToken(ExchangeFunction next) {
return Mono.defer(this::loadClientRegistration)
.map(clientRegistration -> new ClientCredentialsRequest(clientRegistration, audience))
.flatMap(request -> this.auth0InMemoryAccessTokenStore.retrieveToken()
.switchIfEmpty(refreshAuth0Token(request, next)));
}
private Mono<OAuth2AccessToken> refreshAuth0Token(ClientCredentialsRequest clientCredentialsRequest, ExchangeFunction next) {
ClientRegistration clientRegistration = clientCredentialsRequest.getClientRegistration();
String tokenUri = clientRegistration
.getProviderDetails().getTokenUri();
ClientRequest clientCredentialsTokenRequest = ClientRequest.create(HttpMethod.POST, URI.create(tokenUri))
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.body(clientCredentialsTokenBody(clientCredentialsRequest))
.build();
return next.exchange(clientCredentialsTokenRequest)
.flatMap(response -> response.body(oauth2AccessTokenResponse()))
.map(OAuth2AccessTokenResponse::getAccessToken)
.doOnNext(token -> this.auth0InMemoryAccessTokenStore.storeToken(token));
}
private static BodyInserters.FormInserter<String> clientCredentialsTokenBody(ClientCredentialsRequest clientCredentialsRequest) {
ClientRegistration clientRegistration = clientCredentialsRequest.getClientRegistration();
return BodyInserters
.fromFormData("grant_type", AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.with("client_id", clientRegistration.getClientId())
.with("client_secret", clientRegistration.getClientSecret())
.with("audience", clientCredentialsRequest.getAudience());
}
private Mono<ClientRegistration> loadClientRegistration() {
return Mono.just(clientRegistrationId)
.flatMap(r -> clientRegistrationRepository.findByRegistrationId(r));
}
private ClientRequest bearer(ClientRequest request, String token) {
return ClientRequest.from(request)
.headers(headers -> headers.setBearerAuth(token))
.build();
}
static class ClientCredentialsRequest {
private final ClientRegistration clientRegistration;
private final String audience;
public ClientCredentialsRequest(ClientRegistration clientRegistration, String audience) {
this.clientRegistration = clientRegistration;
this.audience = audience;
}
public ClientRegistration getClientRegistration() {
return clientRegistration;
}
public String getAudience() {
return audience;
}
}
}
Магазин токенов
public class Auth0InMemoryAccessTokenStore implements ReactiveInMemoryAccessTokenStore {
private AtomicReference<OAuth2AccessToken> token = new AtomicReference<>();
private Clock clock = Clock.systemUTC();
private Duration accessTokenExpiresSkew = Duration.ofMinutes(1);
public Auth0InMemoryAccessTokenStore() {
}
@Override
public Mono<OAuth2AccessToken> retrieveToken() {
return Mono.justOrEmpty(token.get())
.filter(Objects::nonNull)
.filter(token -> token.getExpiresAt() != null)
.filter(token -> {
Instant now = this.clock.instant();
Instant expiresAt = token.getExpiresAt();
if (now.isBefore(expiresAt.minus(this.accessTokenExpiresSkew))) {
return true;
}
return false;
});
}
@Override
public Mono<Void> storeToken(OAuth2AccessToken token) {
this.token.set(token);
return Mono.empty();
}
}
Интерфейс магазина токенов
public interface ReactiveInMemoryAccessTokenStore {
Mono<OAuth2AccessToken> retrieveToken();
Mono<Void> storeToken(OAuth2AccessToken token);
}
И, наконец, определение компонентов и их использование.
@Bean
public Auth0ClientCredentialsGrantFilterFunction auth0FilterFunction(ReactiveClientRegistrationRepository clientRegistrations,
@Value("${auth0.client-registration-id}") String clientRegistrationId,
@Value("${auth0.audience}") String audience) {
return new Auth0ClientCredentialsGrantFilterFunction(clientRegistrations, clientRegistrationId, audience);
}
@Bean(name = "auth0-webclient")
WebClient webClient(Auth0ClientCredentialsGrantFilterFunction filter) {
return WebClient.builder()
.filter(filter)
.build();
}
В настоящее время существует небольшая проблема с хранилищем токенов, поскольку запрос токена client_credentials будет выполненнесколько параллельных запросов, которые приходят одновременно, но я могу жить с этим в обозримом будущем.