正文中带有额外参数的 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。
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()
行。
好像是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。
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()
行。