diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java index 14d403d9880..0e126b5efa5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java @@ -16,9 +16,6 @@ package org.springframework.security.oauth2.client.endpoint; -import java.util.Collections; -import java.util.Set; - import reactor.core.publisher.Mono; import org.springframework.core.convert.converter.Converter; @@ -30,13 +27,10 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyInserters; -import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec; @@ -54,6 +48,7 @@ * * @param type of grant request * @author Phil Clay + * @author Steve Riesenberg * @since 5.3 * @see RFC-6749 Token * Endpoint @@ -72,7 +67,7 @@ public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient headersConverter = new DefaultOAuth2TokenRequestHeadersConverter<>(); - private Converter> parametersConverter = this::populateTokenRequestParameters; + private Converter> parametersConverter = this::createParameters; private BodyExtractor, ReactiveHttpInputMessage> bodyExtractor = OAuth2BodyExtractors .oauth2AccessTokenResponse(); @@ -86,18 +81,11 @@ public Mono getTokenResponse(T grantRequest) { // @formatter:off return Mono.defer(() -> this.requestEntityConverter.convert(grantRequest) .exchange() - .flatMap((response) -> readTokenResponse(grantRequest, response)) + .flatMap((response) -> response.body(this.bodyExtractor)) ); // @formatter:on } - /** - * Returns the {@link ClientRegistration} for the given {@code grantRequest}. - * @param grantRequest the grant request - * @return the {@link ClientRegistration} for the given {@code grantRequest}. - */ - abstract ClientRegistration clientRegistration(T grantRequest); - private RequestHeadersSpec validatingPopulateRequest(T grantRequest) { validateClientAuthenticationMethod(grantRequest); return populateRequest(grantRequest); @@ -117,128 +105,37 @@ private void validateClientAuthenticationMethod(T grantRequest) { } private RequestHeadersSpec populateRequest(T grantRequest) { + MultiValueMap parameters = this.parametersConverter.convert(grantRequest); return this.webClient.post() - .uri(clientRegistration(grantRequest).getProviderDetails().getTokenUri()) + .uri(grantRequest.getClientRegistration().getProviderDetails().getTokenUri()) .headers((headers) -> { - HttpHeaders headersToAdd = getHeadersConverter().convert(grantRequest); + HttpHeaders headersToAdd = this.headersConverter.convert(grantRequest); if (headersToAdd != null) { headers.addAll(headersToAdd); } }) - .body(createTokenRequestBody(grantRequest)); + .body(BodyInserters.fromFormData(parameters)); } /** - * Populates default parameters for the token request. - * @param grantRequest the grant request - * @return the parameters populated for the token request. + * Returns a {@link MultiValueMap} of the parameters used in the OAuth 2.0 Access + * Token Request body. + * @param grantRequest the authorization grant request + * @return a {@link MultiValueMap} of the parameters used in the OAuth 2.0 Access + * Token Request body */ - private MultiValueMap populateTokenRequestParameters(T grantRequest) { + MultiValueMap createParameters(T grantRequest) { + ClientRegistration clientRegistration = grantRequest.getClientRegistration(); MultiValueMap parameters = new LinkedMultiValueMap<>(); - parameters.add(OAuth2ParameterNames.GRANT_TYPE, grantRequest.getGrantType().getValue()); - return parameters; - } - - /** - * Combine the results of {@code parametersConverter} and - * {@link #populateTokenRequestBody}. - * - *

- * This method pre-populates the body with some standard properties, and then - * delegates to - * {@link #populateTokenRequestBody(AbstractOAuth2AuthorizationGrantRequest, BodyInserters.FormInserter)} - * for subclasses to further populate the body before returning. - *

- * @param grantRequest the grant request - * @return the body for the token request. - */ - private BodyInserters.FormInserter createTokenRequestBody(T grantRequest) { - MultiValueMap parameters = getParametersConverter().convert(grantRequest); - return populateTokenRequestBody(grantRequest, BodyInserters.fromFormData(parameters)); - } - - /** - * Populates the body of the token request. - * - *

- * By default, populates properties that are common to all grant types. Subclasses can - * extend this method to populate grant type specific properties. - *

- * @param grantRequest the grant request - * @param body the body to populate - * @return the populated body - */ - BodyInserters.FormInserter populateTokenRequestBody(T grantRequest, - BodyInserters.FormInserter body) { - ClientRegistration clientRegistration = clientRegistration(grantRequest); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, grantRequest.getGrantType().getValue()); if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC .equals(clientRegistration.getClientAuthenticationMethod())) { - body.with(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId()); + parameters.set(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId()); } if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod())) { - body.with(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); - } - Set scopes = scopes(grantRequest); - if (!CollectionUtils.isEmpty(scopes)) { - body.with(OAuth2ParameterNames.SCOPE, StringUtils.collectionToDelimitedString(scopes, " ")); - } - return body; - } - - /** - * Returns the scopes to include as a property in the token request. - * @param grantRequest the grant request - * @return the scopes to include as a property in the token request. - */ - abstract Set scopes(T grantRequest); - - /** - * Returns the scopes to include in the response if the authorization server returned - * no scopes in the response. - * - *

- * As per RFC-6749 Section - * 5.1 Successful Access Token Response, if AccessTokenResponse.scope is empty, - * then default to the scope originally requested by the client in the Token Request. - *

- * @param grantRequest the grant request - * @return the scopes to include in the response if the authorization server returned - * no scopes. - */ - Set defaultScopes(T grantRequest) { - return Collections.emptySet(); - } - - /** - * Reads the token response from the response body. - * @param grantRequest the request for which the response was received. - * @param response the client response from which to read - * @return the token response from the response body. - */ - private Mono readTokenResponse(T grantRequest, ClientResponse response) { - return response.body(this.bodyExtractor) - .map((tokenResponse) -> populateTokenResponse(grantRequest, tokenResponse)); - } - - /** - * Populates the given {@link OAuth2AccessTokenResponse} with additional details from - * the grant request. - * @param grantRequest the request for which the response was received. - * @param tokenResponse the original token response - * @return a token response optionally populated with additional details from the - * request. - */ - OAuth2AccessTokenResponse populateTokenResponse(T grantRequest, OAuth2AccessTokenResponse tokenResponse) { - if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) { - Set defaultScopes = defaultScopes(grantRequest); - // @formatter:off - tokenResponse = OAuth2AccessTokenResponse - .withResponse(tokenResponse) - .scopes(defaultScopes) - .build(); - // @formatter:on + parameters.set(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); } - return tokenResponse; + return parameters; } /** @@ -247,22 +144,11 @@ OAuth2AccessTokenResponse populateTokenResponse(T grantRequest, OAuth2AccessToke * @param webClient the {@link WebClient} used when requesting the Access Token * Response */ - public void setWebClient(WebClient webClient) { + public final void setWebClient(WebClient webClient) { Assert.notNull(webClient, "webClient cannot be null"); this.webClient = webClient; } - /** - * Returns the {@link Converter} used for converting the - * {@link AbstractOAuth2AuthorizationGrantRequest} instance to a {@link HttpHeaders} - * used in the OAuth 2.0 Access Token Request headers. - * @return the {@link Converter} used for converting the - * {@link AbstractOAuth2AuthorizationGrantRequest} to {@link HttpHeaders} - */ - final Converter getHeadersConverter() { - return this.headersConverter; - } - /** * Sets the {@link Converter} used for converting the * {@link AbstractOAuth2AuthorizationGrantRequest} instance to a {@link HttpHeaders} @@ -305,17 +191,6 @@ public final void addHeadersConverter(Converter headersConverter this.requestEntityConverter = this::populateRequest; } - /** - * Returns the {@link Converter} used for converting the - * {@link AbstractOAuth2AuthorizationGrantRequest} instance to a {@link MultiValueMap} - * used in the OAuth 2.0 Access Token Request body. - * @return the {@link Converter} used for converting the - * {@link AbstractOAuth2AuthorizationGrantRequest} to {@link MultiValueMap} - */ - final Converter> getParametersConverter() { - return this.parametersConverter; - } - /** * Sets the {@link Converter} used for converting the * {@link AbstractOAuth2AuthorizationGrantRequest} instance to a {@link MultiValueMap} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java index ba5ad0cf69a..beadea24cbf 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,11 @@ package org.springframework.security.oauth2.client.endpoint; -import java.util.Collections; -import java.util.Set; - -import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; -import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.util.MultiValueMap; /** * An implementation of a {@link ReactiveOAuth2AccessTokenResponseClient} that @@ -56,32 +51,20 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClient extends AbstractWebClientReactiveOAuth2AccessTokenResponseClient { @Override - ClientRegistration clientRegistration(OAuth2AuthorizationCodeGrantRequest grantRequest) { - return grantRequest.getClientRegistration(); - } - - @Override - Set scopes(OAuth2AuthorizationCodeGrantRequest grantRequest) { - return Collections.emptySet(); - } - - @Override - BodyInserters.FormInserter populateTokenRequestBody(OAuth2AuthorizationCodeGrantRequest grantRequest, - BodyInserters.FormInserter body) { - super.populateTokenRequestBody(grantRequest, body); + MultiValueMap createParameters(OAuth2AuthorizationCodeGrantRequest grantRequest) { OAuth2AuthorizationExchange authorizationExchange = grantRequest.getAuthorizationExchange(); - OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse(); - body.with(OAuth2ParameterNames.CODE, authorizationResponse.getCode()); + MultiValueMap parameters = super.createParameters(grantRequest); + parameters.set(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode()); String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri(); if (redirectUri != null) { - body.with(OAuth2ParameterNames.REDIRECT_URI, redirectUri); + parameters.set(OAuth2ParameterNames.REDIRECT_URI, redirectUri); } String codeVerifier = authorizationExchange.getAuthorizationRequest() .getAttribute(PkceParameterNames.CODE_VERIFIER); if (codeVerifier != null) { - body.with(PkceParameterNames.CODE_VERIFIER, codeVerifier); + parameters.set(PkceParameterNames.CODE_VERIFIER, codeVerifier); } - return body; + return parameters; } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClient.java index f7df261b6b2..07928e7f585 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,12 @@ package org.springframework.security.oauth2.client.endpoint; -import java.util.Set; - import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; /** * An implementation of a {@link ReactiveOAuth2AccessTokenResponseClient} that @@ -45,13 +47,14 @@ public class WebClientReactiveClientCredentialsTokenResponseClient extends AbstractWebClientReactiveOAuth2AccessTokenResponseClient { @Override - ClientRegistration clientRegistration(OAuth2ClientCredentialsGrantRequest grantRequest) { - return grantRequest.getClientRegistration(); - } - - @Override - Set scopes(OAuth2ClientCredentialsGrantRequest grantRequest) { - return grantRequest.getClientRegistration().getScopes(); + MultiValueMap createParameters(OAuth2ClientCredentialsGrantRequest grantRequest) { + ClientRegistration clientRegistration = grantRequest.getClientRegistration(); + MultiValueMap parameters = super.createParameters(grantRequest); + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + parameters.set(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " ")); + } + return parameters; } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveJwtBearerTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveJwtBearerTokenResponseClient.java index 157f00be510..566f17b2a7e 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveJwtBearerTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveJwtBearerTokenResponseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package org.springframework.security.oauth2.client.endpoint; -import java.util.Set; - import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; /** @@ -45,20 +45,15 @@ public final class WebClientReactiveJwtBearerTokenResponseClient extends AbstractWebClientReactiveOAuth2AccessTokenResponseClient { @Override - ClientRegistration clientRegistration(JwtBearerGrantRequest grantRequest) { - return grantRequest.getClientRegistration(); - } - - @Override - Set scopes(JwtBearerGrantRequest grantRequest) { - return grantRequest.getClientRegistration().getScopes(); - } - - @Override - BodyInserters.FormInserter populateTokenRequestBody(JwtBearerGrantRequest grantRequest, - BodyInserters.FormInserter body) { - return super.populateTokenRequestBody(grantRequest, body).with(OAuth2ParameterNames.ASSERTION, - grantRequest.getJwt().getTokenValue()); + MultiValueMap createParameters(JwtBearerGrantRequest grantRequest) { + ClientRegistration clientRegistration = grantRequest.getClientRegistration(); + MultiValueMap parameters = super.createParameters(grantRequest); + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + parameters.set(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " ")); + } + parameters.set(OAuth2ParameterNames.ASSERTION, grantRequest.getJwt().getTokenValue()); + return parameters; } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClient.java index e175b3b37c8..53da7f7cfe5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package org.springframework.security.oauth2.client.endpoint; -import java.util.Set; - import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; /** @@ -52,21 +52,16 @@ public final class WebClientReactivePasswordTokenResponseClient extends AbstractWebClientReactiveOAuth2AccessTokenResponseClient { @Override - ClientRegistration clientRegistration(OAuth2PasswordGrantRequest grantRequest) { - return grantRequest.getClientRegistration(); - } - - @Override - Set scopes(OAuth2PasswordGrantRequest grantRequest) { - return grantRequest.getClientRegistration().getScopes(); - } - - @Override - BodyInserters.FormInserter populateTokenRequestBody(OAuth2PasswordGrantRequest grantRequest, - BodyInserters.FormInserter body) { - return super.populateTokenRequestBody(grantRequest, body) - .with(OAuth2ParameterNames.USERNAME, grantRequest.getUsername()) - .with(OAuth2ParameterNames.PASSWORD, grantRequest.getPassword()); + MultiValueMap createParameters(OAuth2PasswordGrantRequest grantRequest) { + ClientRegistration clientRegistration = grantRequest.getClientRegistration(); + MultiValueMap parameters = super.createParameters(grantRequest); + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + parameters.set(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " ")); + } + parameters.set(OAuth2ParameterNames.USERNAME, grantRequest.getUsername()); + parameters.set(OAuth2ParameterNames.PASSWORD, grantRequest.getPassword()); + return parameters; } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveRefreshTokenTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveRefreshTokenTokenResponseClient.java index 0c814a13e5c..f1cd7471c92 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveRefreshTokenTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveRefreshTokenTokenResponseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,14 @@ package org.springframework.security.oauth2.client.endpoint; -import java.util.Set; +import reactor.core.publisher.Mono; -import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.util.CollectionUtils; -import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; /** @@ -44,29 +44,23 @@ public final class WebClientReactiveRefreshTokenTokenResponseClient extends AbstractWebClientReactiveOAuth2AccessTokenResponseClient { @Override - ClientRegistration clientRegistration(OAuth2RefreshTokenGrantRequest grantRequest) { - return grantRequest.getClientRegistration(); + public Mono getTokenResponse(OAuth2RefreshTokenGrantRequest grantRequest) { + return super.getTokenResponse(grantRequest) + .map((accessTokenResponse) -> populateTokenResponse(grantRequest, accessTokenResponse)); } @Override - Set scopes(OAuth2RefreshTokenGrantRequest grantRequest) { - return grantRequest.getScopes(); - } - - @Override - Set defaultScopes(OAuth2RefreshTokenGrantRequest grantRequest) { - return grantRequest.getAccessToken().getScopes(); - } - - @Override - BodyInserters.FormInserter populateTokenRequestBody(OAuth2RefreshTokenGrantRequest grantRequest, - BodyInserters.FormInserter body) { - return super.populateTokenRequestBody(grantRequest, body).with(OAuth2ParameterNames.REFRESH_TOKEN, - grantRequest.getRefreshToken().getTokenValue()); + MultiValueMap createParameters(OAuth2RefreshTokenGrantRequest grantRequest) { + MultiValueMap parameters = super.createParameters(grantRequest); + if (!CollectionUtils.isEmpty(grantRequest.getScopes())) { + parameters.set(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(grantRequest.getScopes(), " ")); + } + parameters.set(OAuth2ParameterNames.REFRESH_TOKEN, grantRequest.getRefreshToken().getTokenValue()); + return parameters; } - @Override - OAuth2AccessTokenResponse populateTokenResponse(OAuth2RefreshTokenGrantRequest grantRequest, + private OAuth2AccessTokenResponse populateTokenResponse(OAuth2RefreshTokenGrantRequest grantRequest, OAuth2AccessTokenResponse accessTokenResponse) { if (!CollectionUtils.isEmpty(accessTokenResponse.getAccessToken().getScopes()) && accessTokenResponse.getRefreshToken() != null) { @@ -75,7 +69,7 @@ OAuth2AccessTokenResponse populateTokenResponse(OAuth2RefreshTokenGrantRequest g OAuth2AccessTokenResponse.Builder tokenResponseBuilder = OAuth2AccessTokenResponse .withResponse(accessTokenResponse); if (CollectionUtils.isEmpty(accessTokenResponse.getAccessToken().getScopes())) { - tokenResponseBuilder.scopes(defaultScopes(grantRequest)); + tokenResponseBuilder.scopes(grantRequest.getAccessToken().getScopes()); } if (accessTokenResponse.getRefreshToken() == null) { // Reuse existing refresh token diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveTokenExchangeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveTokenExchangeTokenResponseClient.java index abc9ad751b8..92d9b937cb8 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveTokenExchangeTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveTokenExchangeTokenResponseClient.java @@ -16,15 +16,15 @@ package org.springframework.security.oauth2.client.endpoint; -import java.util.Set; - import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2Token; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; /** @@ -51,29 +51,23 @@ public final class WebClientReactiveTokenExchangeTokenResponseClient private static final String JWT_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:jwt"; @Override - ClientRegistration clientRegistration(TokenExchangeGrantRequest grantRequest) { - return grantRequest.getClientRegistration(); - } - - @Override - Set scopes(TokenExchangeGrantRequest grantRequest) { - return grantRequest.getClientRegistration().getScopes(); - } - - @Override - BodyInserters.FormInserter populateTokenRequestBody(TokenExchangeGrantRequest grantRequest, - BodyInserters.FormInserter body) { - super.populateTokenRequestBody(grantRequest, body); - body.with(OAuth2ParameterNames.REQUESTED_TOKEN_TYPE, ACCESS_TOKEN_TYPE_VALUE); + MultiValueMap createParameters(TokenExchangeGrantRequest grantRequest) { + ClientRegistration clientRegistration = grantRequest.getClientRegistration(); + MultiValueMap parameters = super.createParameters(grantRequest); + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + parameters.set(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " ")); + } + parameters.set(OAuth2ParameterNames.REQUESTED_TOKEN_TYPE, ACCESS_TOKEN_TYPE_VALUE); OAuth2Token subjectToken = grantRequest.getSubjectToken(); - body.with(OAuth2ParameterNames.SUBJECT_TOKEN, subjectToken.getTokenValue()); - body.with(OAuth2ParameterNames.SUBJECT_TOKEN_TYPE, tokenType(subjectToken)); + parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN, subjectToken.getTokenValue()); + parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN_TYPE, tokenType(subjectToken)); OAuth2Token actorToken = grantRequest.getActorToken(); if (actorToken != null) { - body.with(OAuth2ParameterNames.ACTOR_TOKEN, actorToken.getTokenValue()); - body.with(OAuth2ParameterNames.ACTOR_TOKEN_TYPE, tokenType(actorToken)); + parameters.set(OAuth2ParameterNames.ACTOR_TOKEN, actorToken.getTokenValue()); + parameters.set(OAuth2ParameterNames.ACTOR_TOKEN_TYPE, tokenType(actorToken)); } - return body; + return parameters; } private static String tokenType(OAuth2Token token) { diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java index cd7c1d31f58..68c4b03ed61 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,6 +48,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; import org.springframework.security.oauth2.jose.TestJwks; @@ -488,6 +489,30 @@ public void convertWhenParametersConverterSetThenCalled() throws Exception { assertThat(actualRequest.getBody().readUtf8()).contains("custom-parameter-name=custom-parameter-value"); } + @Test + public void getTokenResponseWhenParametersConverterSetThenAbleToOverrideDefaultParameters() throws Exception { + this.clientRegistration.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST); + OAuth2AuthorizationCodeGrantRequest request = authorizationCodeGrantRequest(); + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" + + " \"token_type\":\"bearer\",\n" + + " \"expires_in\":3600,\n" + + " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\"\n" + + "}"; + // @formatter:on + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, "custom"); + parameters.set(OAuth2ParameterNames.CODE, "custom-code"); + parameters.set(OAuth2ParameterNames.REDIRECT_URI, "custom-uri"); + // The client_id parameter is omitted for testing purposes + this.tokenResponseClient.setParametersConverter((grantRequest) -> parameters); + this.tokenResponseClient.getTokenResponse(request).block(); + String body = this.server.takeRequest().getBody().readUtf8(); + assertThat(body).contains("grant_type=custom", "code=custom-code", "redirect_uri=custom-uri"); + } + // gh-10260 @Test public void getTokenResponseWhenSuccessCustomResponseThenReturnAccessTokenResponse() { diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClientTests.java index 380240b1892..b8de3194ac1 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.jose.TestKeys; @@ -428,6 +429,29 @@ public void convertWhenParametersConverterSetThenCalled() throws Exception { assertThat(actualRequest.getBody().readUtf8()).contains("custom-parameter-name=custom-parameter-value"); } + @Test + public void getTokenResponseWhenParametersConverterSetThenAbleToOverrideDefaultParameters() throws Exception { + this.clientRegistration.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST); + OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest( + this.clientRegistration.build()); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, "custom"); + parameters.set(OAuth2ParameterNames.SCOPE, "one two"); + // The client_id parameter is omitted for testing purposes + this.client.setParametersConverter((grantRequest) -> parameters); + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" + + " \"token_type\":\"bearer\",\n" + + " \"expires_in\":3600\n" + + "}"; + // @formatter:on + enqueueJson(accessTokenSuccessResponse); + this.client.getTokenResponse(request).block(); + String body = this.server.takeRequest().getBody().readUtf8(); + assertThat(body).contains("grant_type=custom", "scope=one+two"); + } + // gh-10260 @Test public void getTokenResponseWhenSuccessCustomResponseThenReturnAccessTokenResponse() { diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveJwtBearerTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveJwtBearerTokenResponseClientTests.java index b615c4924b7..c8ed9b08d2d 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveJwtBearerTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveJwtBearerTokenResponseClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.TestJwts; @@ -277,6 +278,23 @@ public void convertWhenParametersConverterSetThenCalled() throws Exception { assertThat(actualRequest.getBody().readUtf8()).contains("custom-parameter-name=custom-parameter-value"); } + @Test + public void getTokenResponseWhenParametersConverterSetThenAbleToOverrideDefaultParameters() throws Exception { + this.clientRegistration.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST); + ClientRegistration clientRegistration = this.clientRegistration.build(); + JwtBearerGrantRequest request = new JwtBearerGrantRequest(clientRegistration, this.jwtAssertion); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, "custom"); + parameters.set(OAuth2ParameterNames.ASSERTION, "custom-assertion"); + parameters.set(OAuth2ParameterNames.SCOPE, "one two"); + // The client_id parameter is omitted for testing purposes + this.client.setParametersConverter((grantRequest) -> parameters); + enqueueJson(DEFAULT_ACCESS_TOKEN_RESPONSE); + this.client.getTokenResponse(request).block(); + String body = this.server.takeRequest().getBody().readUtf8(); + assertThat(body).contains("grant_type=custom", "scope=one+two", "assertion=custom-assertion"); + } + @Test public void getTokenResponseWhenBodyExtractorSetThenCalled() { BodyExtractor, ReactiveHttpInputMessage> bodyExtractor = mock( diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java index da8b2353193..d74663ff54b 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactivePasswordTokenResponseClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.jose.TestKeys; @@ -473,6 +474,32 @@ public void convertWhenParametersConverterSetThenCalled() throws Exception { assertThat(actualRequest.getBody().readUtf8()).contains("custom-parameter-name=custom-parameter-value"); } + @Test + public void getTokenResponseWhenParametersConverterSetThenAbleToOverrideDefaultParameters() throws Exception { + this.clientRegistrationBuilder.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST); + OAuth2PasswordGrantRequest request = new OAuth2PasswordGrantRequest(this.clientRegistrationBuilder.build(), + this.username, this.password); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, "custom"); + parameters.set(OAuth2ParameterNames.USERNAME, "user"); + parameters.set(OAuth2ParameterNames.PASSWORD, "password"); + parameters.set(OAuth2ParameterNames.SCOPE, "one two"); + // The client_id parameter is omitted for testing purposes + this.tokenResponseClient.setParametersConverter((grantRequest) -> parameters); + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" + + " \"token_type\":\"bearer\",\n" + + " \"expires_in\":3600,\n" + + " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\"\n" + + "}"; + // @formatter:on + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + this.tokenResponseClient.getTokenResponse(request).block(); + String body = this.server.takeRequest().getBody().readUtf8(); + assertThat(body).contains("grant_type=custom", "scope=one+two", "username=user", "password=password"); + } + // gh-10260 @Test public void getTokenResponseWhenSuccessCustomResponseThenReturnAccessTokenResponse() { diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveRefreshTokenTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveRefreshTokenTokenResponseClientTests.java index 204080be82a..561763717fe 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveRefreshTokenTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveRefreshTokenTokenResponseClientTests.java @@ -46,6 +46,7 @@ import org.springframework.security.oauth2.core.TestOAuth2AccessTokens; import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; import org.springframework.security.oauth2.jose.TestJwks; import org.springframework.security.oauth2.jose.TestKeys; @@ -442,6 +443,31 @@ public void convertWhenParametersConverterSetThenCalled() throws Exception { assertThat(actualRequest.getBody().readUtf8()).contains("custom-parameter-name=custom-parameter-value"); } + @Test + public void getTokenResponseWhenParametersConverterSetThenAbleToOverrideDefaultParameters() throws Exception { + this.clientRegistrationBuilder.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST); + OAuth2RefreshTokenGrantRequest request = new OAuth2RefreshTokenGrantRequest( + this.clientRegistrationBuilder.build(), this.accessToken, this.refreshToken); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, "custom"); + parameters.set(OAuth2ParameterNames.REFRESH_TOKEN, "custom-token"); + parameters.set(OAuth2ParameterNames.SCOPE, "one two"); + // The client_id parameter is omitted for testing purposes + this.tokenResponseClient.setParametersConverter((grantRequest) -> parameters); + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" + + " \"token_type\":\"bearer\",\n" + + " \"expires_in\":3600,\n" + + " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\"\n" + + "}"; + // @formatter:on + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + this.tokenResponseClient.getTokenResponse(request).block(); + String body = this.server.takeRequest().getBody().readUtf8(); + assertThat(body).contains("grant_type=custom", "refresh_token=custom-token", "scope=one+two"); + } + // gh-10260 @Test public void getTokenResponseWhenSuccessCustomResponseThenReturnAccessTokenResponse() { diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveTokenExchangeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveTokenExchangeTokenResponseClientTests.java index 2e3d32bb170..508fb4d92b8 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveTokenExchangeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveTokenExchangeTokenResponseClientTests.java @@ -567,6 +567,30 @@ public void getTokenResponseWhenParametersConverterSetThenCalled() throws Except assertThat(formParameters).contains("custom-parameter-name=custom-parameter-value"); } + @Test + public void getTokenResponseWhenParametersConverterSetThenAbleToOverrideDefaultParameters() throws Exception { + this.clientRegistration.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, "custom"); + parameters.set(OAuth2ParameterNames.SCOPE, "one two"); + parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN, "custom-token"); + // The client_id parameter is omitted for testing purposes + this.tokenResponseClient.setParametersConverter((request) -> parameters); + // @formatter:off + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + // @formatter:on + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + TokenExchangeGrantRequest grantRequest = new TokenExchangeGrantRequest(this.clientRegistration.build(), + this.subjectToken, this.actorToken); + this.tokenResponseClient.getTokenResponse(grantRequest).block(); + String body = this.server.takeRequest().getBody().readUtf8(); + assertThat(body).contains("grant_type=custom", "scope=one+two", "subject_token=custom-token"); + } + @Test public void getTokenResponseWhenParametersConverterAddedThenCalled() throws Exception { // @formatter:off