OAuth2客户端,在body (Audience)中有额外的参数



似乎在使用Auth0时,在M2M流中,我们需要在授权请求中传递audience参数,并且将为该audience颁发令牌

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 Security 5方法与webflux使用WebClient实现Client Credentials流。

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

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

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

thanks to 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公共类privatcontroller {

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

自从上次回答以来,有几个类被弃用了,我在Spring 2.7.13/3.1.1中发布了我的解决方案

@Bean("apiGatewayWebClient")
public WebClient apiGatewayWebClient(ReactiveClientRegistrationRepository clientRegistrations) {
InMemoryReactiveOAuth2AuthorizedClientService clientService =
new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations);
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider());
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth.setDefaultClientRegistrationId("adfs");
return WebClient.builder()
.filter(oauth)
.build();
}
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider() {
WebClientReactiveClientCredentialsTokenResponseClient responseClient =
new WebClientReactiveClientCredentialsTokenResponseClient();
// Spring does not provide a configuration only way to add 'resource' to the message body
// See https://docs.spring.io/spring-security/reference/servlet/oauth2/client/authorization-grants.html#_customizing_the_access_token_request_2
responseClient.addParametersConverter(
source -> {
LinkedMultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("audience", "myAudience");
return map;
}
);
return ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(clientCredentialsGrantBuilder ->
clientCredentialsGrantBuilder.accessTokenResponseClient(responseClient))
.build();
}

与application.yml

spring:
security:
oauth2:
client:
registration:
adfs:
client-id: 'client:id'
client-secret: '???????'
authorization-grant-type: 'client_credentials'
provider:
adfs:
token-uri: 'https://domain.eu.auth0.com/oauth/token'

clientCredentials()的参数是一个生成器Consumer。这意味着您提供的函数将生成器作为参数,然后您需要使用该参数进行进一步配置,即配置它以使用新创建的客户端。在代码中,您没有使用构建器做任何事情,因此,无论您在函数中创建什么,都只是一个永远不会使用的局部变量。

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

注意clientCredentialsGrantBuilder.accessTokenResponseClient()行。

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

Servletxxxxx只在阻塞环境下工作,如果我没记错的话,但大多数都有Serverxxxxx替代非阻塞。

我维护一个Spring Boot启动器,它允许在应用程序属性中定义authorization_code请求的任何附加参数:

<dependency>
<groupId>org.springframework.boot</groupId>
<!-- For a reactive application, use spring-boot-starter-webflux instead -->
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc</artifactId>
</dependency>
auth0-issuer: https://domain.eu.auth0.com/
auth0-client-id: change-me
auth0-client-secret: change-me
auth0-aud: http://localhost:8080
spring:
security:
oauth2:
client:
provider:
auth0:
issuer-uri: ${auth0-issuer}
registration:
auth0-authorization-code:
authorization-grant-type: authorization_code
client-id: ${auth0-client-id}
client-secret: ${auth0-client-secret}
provider: auth0
scope: openid,profile,email,offline_access
com:
c4-soft:
springaddons:
oidc:
ops:
- iss: ${auth0-issuer}
# You can replace it to a JSON path to any claim, even a nested private claim like $['https://c4-soft.com']['name']
username-claim: $.sub
authorities:
- path: roles
- path: permissions
- path: $['https://c4-soft.com']['authorities']
client:
security-matchers:
- /**
permit-all:
- /login/**
- /oauth2/**
- /
# By default, Auth0 does not follow strictly the OpenID RP-Initiated Logout spec and needs specific configuration
oauth2-logout:
auth0-authorization-code:
uri: ${auth0-issuer}v2/logout
client-id-request-param: client_id
post-logout-uri-request-param: returnTo
# Auth0 requires an "audience" parameter in authorization-code request to deliver JWTs
authorization-request-params:
auth0-authorization-code:
- name: audience
value: ${auth0-aud}

就是这样,不需要Java配置。

上面链接的repo教程部分包括一个Auth0配置README,其中包含创建"动作"的说明;向访问令牌添加用户详细信息。

最新更新