正文中带有额外参数的 OAuth2 客户端(受众)

OAuth2 client with extra parameters in body (Audience)

好像是Auth0,在M2M流程中,我们需要在授权请求中传递audience参数,然后会为这样的audience[=24=颁发令牌]

curl --request POST \
  --url https://domain.eu.auth0.com/oauth/token \
  --header 'content-type: application/json' \
  --data '{"client_id":"xxxxx","client_secret":"xxxxx","audience":"my-api-audience","grant_type":"client_credentials"}'

否则会抛出错误

403 Forbidden: "{"error":"access_denied","error_description":"No audience parameter was provided, and no default audience has been configured"}"

我尝试通过 Spring Boot 实现 Client Credentials 流程,使用新的 Spring Security 5 方法和使用 WebClient 的 webflux。

https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/oauth2/webclient

Spring 不提供向 Auth 请求添加自定义参数的方法,因此 post

https://github.com/spring-projects/spring-security/issues/6569

我必须实现自定义转换器。

一切似乎都在启动时注入正常,但在访问客户端端点时从未调用转换localhost/api/explicit所以我一直陷入audience问题。

WebClientConfig.java

@Configuration
public class WebClientConfig {
    @Value("${resource-uri}")
    String resourceUri;

    @Value("${wallet-audience}")
    String audience;

       @Bean
        WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {

            var oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);

            return WebClient.builder()
                    .filter(oauth2)
                    // TRIED BOTH
                    //.apply(oauth2.oauth2Configuration())
                    .build();
        }

        @Bean
        OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) {

            Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> customRequestEntityConverter = new Auth0ClientCredentialsGrantRequestEntityConverter(audience);

            // @formatter:off
            var authorizedClientProvider =
                    OAuth2AuthorizedClientProviderBuilder.builder()
                                                         .refreshToken()
                                                         .clientCredentials(clientCredentialsGrantBuilder -> {
                                                             var clientCredentialsTokenResponseClient = new DefaultClientCredentialsTokenResponseClient();
                                                             clientCredentialsTokenResponseClient.setRequestEntityConverter(customRequestEntityConverter);
                                                         })
                                                         .build();
            // @formatter:on

            var authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
            authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

            return authorizedClientManager;
        }

Auth0ClientCredentialsGrantRequestEntityConverter.java

感谢 https://www.aheritier.net/spring-boot-app-client-of-an-auth0-protected-service-jwt/

import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.Collections;

public final class Auth0ClientCredentialsGrantRequestEntityConverter implements Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> {

    private static final HttpHeaders DEFAULT_TOKEN_REQUEST_HEADERS = getDefaultTokenRequestHeaders();

    private final String audience;

    /**
     * @param audience The audience to pass to Auth0
     */
    public Auth0ClientCredentialsGrantRequestEntityConverter(String audience) {
        this.audience = audience;
    }

    /**
     * Returns the {@link RequestEntity} used for the Access Token Request.
     *
     * @param clientCredentialsGrantRequest the client credentials grant request
     * @return the {@link RequestEntity} used for the Access Token Request
     */
    @Override
    public RequestEntity<?> convert(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
        var clientRegistration = clientCredentialsGrantRequest.getClientRegistration();
        var headers = getTokenRequestHeaders(clientRegistration);
        var formParameters = this.buildFormParameters(clientCredentialsGrantRequest);
        var uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
                                      .build()
                                      .toUri();
        return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
    }

    /**
     * Returns a {@link MultiValueMap} of the form parameters used for the Access Token
     * Request body.
     *
     * @param clientCredentialsGrantRequest the client credentials grant request
     * @return a {@link MultiValueMap} of the form parameters used for the Access Token
     * Request body
     */
    private MultiValueMap<String, String> buildFormParameters(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
        var clientRegistration = clientCredentialsGrantRequest.getClientRegistration();
        MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
        formParameters.add(OAuth2ParameterNames.GRANT_TYPE, clientCredentialsGrantRequest.getGrantType().getValue());

        if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
            formParameters.add(OAuth2ParameterNames.SCOPE,
                    StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " "));
        }

        if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
            formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
            formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
        }

        formParameters.add("audience", this.audience);
        return formParameters;
    }

    private static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration) {
        var headers = new HttpHeaders();
        headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);
        if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
            headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
        }
        return headers;
    }

    private static HttpHeaders getDefaultTokenRequestHeaders() {
        var headers = new HttpHeaders();
        final var contentType = MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");

        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        headers.setContentType(contentType);

        return headers;
    }

}

Controller.java

@RestController public class PrivateController {

private final WebClient webClient;

public PrivateController(WebClient webClient) {
    this.webClient = webClient;
}

@GetMapping("/explicit")
String explicit(Model model, @RegisteredOAuth2AuthorizedClient("wallet") OAuth2AuthorizedClient authorizedClient) {

    String body = this.webClient
            .get()
            .attributes(oauth2AuthorizedClient(authorizedClient))
            .retrieve()
            .bodyToMono(String.class)
            .block();

    model.addAttribute("body", body);
    return "response";
}

}

application.properties

spring.security.oauth2.client.registration.wallet.client-id                = 
spring.security.oauth2.client.registration.wallet.client-secret            =
spring.security.oauth2.client.registration.wallet.scope[]                  = read:transaction,write:transaction
spring.security.oauth2.client.registration.wallet.authorization-grant-type = client_credentials

spring.security.oauth2.client.provider.wallet.issuer-uri                   = https://domain.eu.auth0.com/

resource-uri                                                               = http://localhost:8081/api/wallet
wallet-audience                                                            = https://wallet

我认为您需要为 WebClient 配置使用 ServerOAuth2AuthorizedClientExchangeFilterFunction 而不是 ServletOAuth2AuthorizedClientExchangeFilterFunction

Servletxxxxx 如果我没记错的话,只能在阻塞环境中工作,但大多数都有 Serverxxxxx 替代 non-blocking..

clientCredentials() 的参数是生成器 Consumer。这意味着您提供的函数将构建器作为参数,然后您需要将其用于进一步的配置,即将其配置为使用新创建的客户端。您没有对代码中的构建器执行任何操作,因此您在函数中创建的任何内容都只是一个从未使用过的局部变量。

var authorizedClientProvider =
       OAuth2AuthorizedClientProviderBuilder.builder()
                 .refreshToken()
                 .clientCredentials(clientCredentialsGrantBuilder -> {
                             var clientCredentialsTokenResponseClient = new DefaultClientCredentialsTokenResponseClient();
                             clientCredentialsTokenResponseClient.setRequestEntityConverter(customRequestEntityConverter); 
                             clientCredentialsGrantBuilder.accessTokenResponseClient(clientCredentialsTokenResponseClient);
                  })
                 .build();

注意 clientCredentialsGrantBuilder.accessTokenResponseClient() 行。