Spring Boot добавляет дополнительный атрибут к запросу WebClient в ServerOAuth2AuthorizedClientExchangeFilterFunction - PullRequest
0 голосов
/ 28 ноября 2018

Я пытаюсь реализовать грант client_credentials для получения токена на моем сервере ресурсов весенней загрузки.Я использую Auth0 в качестве сервера авторизации.Кажется, они требуют добавления дополнительного параметра в теле запроса, называемого аудиторией.

Я попытался выполнить запрос через почтальона, и он работает.Я сейчас пытаюсь воспроизвести его в течение весны.Вот рабочий запрос почтальона

curl -X POST \
  https://XXX.auth0.com/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials&audience=https%3A%2F%2Fxxxxx.auth0.com%2Fapi%2Fv2%2F&client_id=SOME_CLIENT_ID&client_secret=SOME_CLIENT_SECRET'

Проблема, с которой я сталкиваюсь, заключается в том, что у меня нет возможности добавить отсутствующий параметр аудитории в запрос токена.

У меня есть конфигурация, определенная в моемapplication.yml

client:
    provider:
      auth0:
        issuer-uri: https://XXXX.auth0.com//
    registration:
      auth0-client:
        provider: auth0
        client-id: Client
        client-secret: Secret
        authorization_grant_type: client_credentials
      auth0:
        client-id: Client
        client-secret: Secret

У меня настроен фильтр веб-клиента следующим образом.

@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
                    ServerOAuth2AuthorizedClientRepository authorizedClients) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
            clientRegistrations, authorizedClients);
    oauth2.setDefaultClientRegistrationId("auth0");
    return WebClient.builder()
            .filter(oauth2)
            .build();
}

Я внедряю экземпляр и пытаюсь сделать запрос, чтобы получить пользователя по электронной почте

 return this.webClient.get()
            .uri(this.usersUrl + "/api/v2/users-by-email?email={email}", email)
            .attributes(auth0ClientCredentials())
            .retrieve()
            .bodyToMono(User.class);

Насколько я понимаю, фильтр перехватывает этот запрос userByEmail и, прежде чем выполнить его, пытается выполнить запрос / oauth / token, чтобы получить токен JWT Bearer, который он может добавить к первому и выполнить его..

Есть ли способ добавить параметр в фильтр?Было чрезвычайно трудно пройти через это и выяснить, где именно параметры добавляются с момента его реагирования, и я довольно нов в этом.Даже некоторые указатели на то, где искать, будут полезны.

Ответы [ 2 ]

0 голосов
/ 30 ноября 2018

Вот что я узнал после дальнейшего расследования.Код, описанный в моем вопросе, никогда не собирался вызывать 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 будет выполненнесколько параллельных запросов, которые приходят одновременно, но я могу жить с этим в обозримом будущем.

0 голосов
/ 28 ноября 2018

Прямо сейчас это возможно, но не элегантно.

Обратите внимание, что вы можете предоставить пользовательский ReactiveOAuth2AccessTokenResponseClient - ServerOAuth2AuthorizedClientExchangeFilterFunction.

. Вы можете создать собственную реализацию этого - и тем самым добавить любые другие необходимые параметры - скопировав содержимоеWebClientReactiveClientCredentialsTokenResponseClient.

Тем не менее, было бы лучше, если бы был установщик, чтобы сделать это более удобным.Вы можете следить за соответствующей проблемой в журнале ожидания Spring Security.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...