From eff4cdc9241d264376e06ce0117c5a6715f77094 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Wed, 31 Mar 2021 10:41:59 -0400 Subject: [PATCH] Polish gh-9505 --- ...tBearerOAuth2AuthorizedClientProvider.java | 91 +++++------ .../client/OAuth2AuthorizationContext.java | 8 +- ...OAuth2AuthorizedClientProviderBuilder.java | 99 +----------- .../DefaultJwtBearerTokenResponseClient.java | 60 +++---- ...equest.java => JwtBearerGrantRequest.java} | 39 ++--- .../JwtBearerGrantRequestEntityConverter.java | 62 ++++++++ ...2JwtBearerGrantRequestEntityConverter.java | 92 ----------- ...erOAuth2AuthorizedClientProviderTests.java | 130 +++++---------- ...2AuthorizedClientProviderBuilderTests.java | 24 --- ...aultJwtBearerTokenResponseClientTests.java | 117 ++++++++++---- ...earerGrantRequestEntityConverterTests.java | 148 ++++++++++++++++++ ...s.java => JwtBearerGrantRequestTests.java} | 20 ++- ...earerGrantRequestEntityConverterTests.java | 82 ---------- .../registration/TestClientRegistrations.java | 13 -- ...izedClientExchangeFilterFunctionTests.java | 59 ++++++- .../oauth2/core/AuthorizationGrantType.java | 3 + .../core/endpoint/OAuth2ParameterNames.java | 11 +- .../core/AuthorizationGrantTypeTests.java | 8 +- 18 files changed, 491 insertions(+), 575 deletions(-) rename oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/{OAuth2JwtBearerGrantRequest.java => JwtBearerGrantRequest.java} (56%) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequestEntityConverter.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequestEntityConverter.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequestEntityConverterTests.java rename oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/{OAuth2JwtBearerGrantRequestTests.java => JwtBearerGrantRequestTests.java} (76%) delete mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequestEntityConverterTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java index 694c6f023d..ef10998ce9 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java @@ -16,23 +16,20 @@ package org.springframework.security.oauth2.client; -import java.time.Clock; -import java.time.Duration; -import java.time.Instant; - import org.springframework.lang.Nullable; import org.springframework.security.oauth2.client.endpoint.DefaultJwtBearerTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.OAuth2JwtBearerGrantRequest; import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.util.Assert; /** * An implementation of an {@link OAuth2AuthorizedClientProvider} for the - * {@link OAuth2JwtBearerGrantRequest#JWT_BEARER_GRANT_TYPE jwt-bearer} grant. + * {@link AuthorizationGrantType#JWT_BEARER jwt-bearer} grant. * * @author Joe Grandja * @since 5.5 @@ -41,18 +38,14 @@ import org.springframework.util.Assert; */ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider { - private OAuth2AccessTokenResponseClient accessTokenResponseClient = new DefaultJwtBearerTokenResponseClient(); - - private Duration clockSkew = Duration.ofSeconds(60); - - private Clock clock = Clock.systemUTC(); + private OAuth2AccessTokenResponseClient accessTokenResponseClient = new DefaultJwtBearerTokenResponseClient(); /** * Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration() * client} in the provided {@code context}. Returns {@code null} if authorization is * not supported, e.g. the client's * {@link ClientRegistration#getAuthorizationGrantType() authorization grant type} is - * not {@link OAuth2JwtBearerGrantRequest#JWT_BEARER_GRANT_TYPE jwt-bearer}. + * not {@link AuthorizationGrantType#JWT_BEARER jwt-bearer}. * @param context the context that holds authorization-specific state for the client * @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not * supported @@ -61,34 +54,45 @@ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2Auth @Nullable public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { Assert.notNull(context, "context cannot be null"); - ClientRegistration clientRegistration = context.getClientRegistration(); - if (!OAuth2JwtBearerGrantRequest.JWT_BEARER_GRANT_TYPE.equals(clientRegistration.getAuthorizationGrantType())) { + if (!AuthorizationGrantType.JWT_BEARER.equals(clientRegistration.getAuthorizationGrantType())) { return null; } - - Jwt jwt = context.getAttribute(OAuth2AuthorizationContext.JWT_ATTRIBUTE_NAME); - if (jwt == null) { - return null; - } - OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient(); - if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) { - // If client is already authorized but access token is NOT expired than no - // need for re-authorization + if (authorizedClient != null) { + // Client is already authorized return null; } - - OAuth2JwtBearerGrantRequest jwtBearerGrantRequest = new OAuth2JwtBearerGrantRequest(clientRegistration, jwt); - OAuth2AccessTokenResponse tokenResponse = this.accessTokenResponseClient - .getTokenResponse(jwtBearerGrantRequest); - + if (!(context.getPrincipal().getPrincipal() instanceof Jwt)) { + return null; + } + Jwt jwt = (Jwt) context.getPrincipal().getPrincipal(); + // As per spec, in section 4.1 Using Assertions as Authorization Grants + // https://tools.ietf.org/html/rfc7521#section-4.1 + // + // An assertion used in this context is generally a short-lived + // representation of the authorization grant, and authorization servers + // SHOULD NOT issue access tokens with a lifetime that exceeds the + // validity period of the assertion by a significant period. In + // practice, that will usually mean that refresh tokens are not issued + // in response to assertion grant requests, and access tokens will be + // issued with a reasonably short lifetime. Clients can refresh an + // expired access token by requesting a new one using the same + // assertion, if it is still valid, or with a new assertion. + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(clientRegistration, jwt); + OAuth2AccessTokenResponse tokenResponse = getTokenResponse(clientRegistration, jwtBearerGrantRequest); return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(), tokenResponse.getAccessToken()); } - private boolean hasTokenExpired(AbstractOAuth2Token token) { - return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew)); + private OAuth2AccessTokenResponse getTokenResponse(ClientRegistration clientRegistration, + JwtBearerGrantRequest jwtBearerGrantRequest) { + try { + return this.accessTokenResponseClient.getTokenResponse(jwtBearerGrantRequest); + } + catch (OAuth2AuthorizationException ex) { + throw new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex); + } } /** @@ -98,32 +102,9 @@ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2Auth * credential at the Token Endpoint for the {@code jwt-bearer} grant */ public void setAccessTokenResponseClient( - OAuth2AccessTokenResponseClient accessTokenResponseClient) { + OAuth2AccessTokenResponseClient accessTokenResponseClient) { Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null"); this.accessTokenResponseClient = accessTokenResponseClient; } - /** - * Sets the maximum acceptable clock skew, which is used when checking the - * {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is - * 60 seconds. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. - * @param clockSkew the maximum acceptable clock skew - */ - public void setClockSkew(Duration clockSkew) { - Assert.notNull(clockSkew, "clockSkew cannot be null"); - Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0"); - this.clockSkew = clockSkew; - } - - /** - * Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access - * token expiry. - * @param clock the clock - */ - public void setClock(Clock clock) { - Assert.notNull(clock, "clock cannot be null"); - this.clock = clock; - } - } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java index 794bbb1223..a74a319d69 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2019 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. @@ -60,12 +60,6 @@ public final class OAuth2AuthorizationContext { */ public static final String PASSWORD_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".PASSWORD"); - /** - * The name of the {@link #getAttribute(String) attribute} in the context associated - * to the value for the JWT Bearer token. - */ - public static final String JWT_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".JWT"); - private ClientRegistration clientRegistration; private OAuth2AuthorizedClient authorizedClient; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java index 5aaef72bf7..fa109dd2aa 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2019 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. @@ -27,7 +27,6 @@ import java.util.function.Consumer; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2JwtBearerGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; import org.springframework.util.Assert; @@ -157,29 +156,6 @@ public final class OAuth2AuthorizedClientProviderBuilder { return OAuth2AuthorizedClientProviderBuilder.this; } - /** - * Configures support for the {@code jwt_bearer} grant. - * @return the {@link OAuth2AuthorizedClientProviderBuilder} - */ - public OAuth2AuthorizedClientProviderBuilder jwtBearer() { - this.builders.computeIfAbsent(JwtBearerOAuth2AuthorizedClientProvider.class, - (k) -> new JwtBearerGrantBuilder()); - return OAuth2AuthorizedClientProviderBuilder.this; - } - - /** - * Configures support for the {@code jwt_bearer} grant. - * @param builderConsumer a {@code Consumer} of {@link JwtBearerGrantBuilder} used for - * further configuration - * @return the {@link OAuth2AuthorizedClientProviderBuilder} - */ - public OAuth2AuthorizedClientProviderBuilder jwtBearer(Consumer builderConsumer) { - JwtBearerGrantBuilder builder = (JwtBearerGrantBuilder) this.builders - .computeIfAbsent(JwtBearerOAuth2AuthorizedClientProvider.class, (k) -> new JwtBearerGrantBuilder()); - builderConsumer.accept(builder); - return OAuth2AuthorizedClientProviderBuilder.this; - } - /** * Builds an instance of {@link DelegatingOAuth2AuthorizedClientProvider} composed of * one or more {@link OAuth2AuthorizedClientProvider}(s). @@ -229,7 +205,7 @@ public final class OAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) + clockSkew}. + * {@code Instant.now(this.clock) - clockSkew}. * @param clockSkew the maximum acceptable clock skew * @return the {@link PasswordGrantBuilder} */ @@ -270,77 +246,6 @@ public final class OAuth2AuthorizedClientProviderBuilder { } - /** - * A builder for the {@code jwt_bearer} grant. - */ - public final class JwtBearerGrantBuilder implements Builder { - - private OAuth2AccessTokenResponseClient accessTokenResponseClient; - - private Duration clockSkew; - - private Clock clock; - - private JwtBearerGrantBuilder() { - } - - /** - * Sets the client used when requesting an access token credential at the Token - * Endpoint. - * @param accessTokenResponseClient the client used when requesting an access - * token credential at the Token Endpoint - * @return the {@link JwtBearerGrantBuilder} - */ - public JwtBearerGrantBuilder accessTokenResponseClient( - OAuth2AccessTokenResponseClient accessTokenResponseClient) { - this.accessTokenResponseClient = accessTokenResponseClient; - return this; - } - - /** - * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) + clockSkew}. - * @param clockSkew the maximum acceptable clock skew - * @return the {@link JwtBearerGrantBuilder} - */ - public JwtBearerGrantBuilder clockSkew(Duration clockSkew) { - this.clockSkew = clockSkew; - return this; - } - - /** - * Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the - * access token expiry. - * @param clock the clock - * @return the {@link JwtBearerGrantBuilder} - */ - public JwtBearerGrantBuilder clock(Clock clock) { - this.clock = clock; - return this; - } - - /** - * Builds an instance of {@link JwtBearerOAuth2AuthorizedClientProvider}. - * @return the {@link JwtBearerOAuth2AuthorizedClientProvider} - */ - @Override - public OAuth2AuthorizedClientProvider build() { - JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider(); - if (this.accessTokenResponseClient != null) { - authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient); - } - if (this.clockSkew != null) { - authorizedClientProvider.setClockSkew(this.clockSkew); - } - if (this.clock != null) { - authorizedClientProvider.setClock(this.clock); - } - return authorizedClientProvider; - } - - } - /** * A builder for the {@code client_credentials} grant. */ diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClient.java index 453d40b04b..419b5f91d6 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClient.java @@ -24,6 +24,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; @@ -37,24 +38,26 @@ import org.springframework.web.client.RestTemplate; /** * The default implementation of an {@link OAuth2AccessTokenResponseClient} for the - * {@link OAuth2JwtBearerGrantRequest#JWT_BEARER_GRANT_TYPE jwt-bearer} grant. This - * implementation uses a {@link RestOperations} when requesting an access token credential - * at the Authorization Server's Token Endpoint. + * {@link AuthorizationGrantType#JWT_BEARER jwt-bearer} grant. This implementation uses a + * {@link RestOperations} when requesting an access token credential at the Authorization + * Server's Token Endpoint. * * @author Joe Grandja * @since 5.5 * @see OAuth2AccessTokenResponseClient - * @see OAuth2JwtBearerGrantRequest + * @see JwtBearerGrantRequest * @see OAuth2AccessTokenResponse * @see Section - * 2.1 JWTs as Authorization Grants + * 2.1 Using JWTs as Authorization Grants + * @see Section + * 4.1 Using Assertions as Authorization Grants */ public final class DefaultJwtBearerTokenResponseClient - implements OAuth2AccessTokenResponseClient { + implements OAuth2AccessTokenResponseClient { private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response"; - private Converter> requestEntityConverter = new OAuth2JwtBearerGrantRequestEntityConverter(); + private Converter> requestEntityConverter = new JwtBearerGrantRequestEntityConverter(); private RestOperations restOperations; @@ -66,14 +69,28 @@ public final class DefaultJwtBearerTokenResponseClient } @Override - public OAuth2AccessTokenResponse getTokenResponse(OAuth2JwtBearerGrantRequest jwtBearerGrantRequest) { + public OAuth2AccessTokenResponse getTokenResponse(JwtBearerGrantRequest jwtBearerGrantRequest) { Assert.notNull(jwtBearerGrantRequest, "jwtBearerGrantRequest cannot be null"); - RequestEntity request = this.requestEntityConverter.convert(jwtBearerGrantRequest); + ResponseEntity response = getResponse(request); + OAuth2AccessTokenResponse tokenResponse = response.getBody(); + if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) { + // As per spec, in Section 5.1 Successful Access Token Response + // https://tools.ietf.org/html/rfc6749#section-5.1 + // If AccessTokenResponse.scope is empty, then default to the scope + // originally requested by the client in the Token Request + // @formatter:off + tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) + .scopes(jwtBearerGrantRequest.getClientRegistration().getScopes()) + .build(); + // @formatter:on + } + return tokenResponse; + } - ResponseEntity response; + private ResponseEntity getResponse(RequestEntity request) { try { - response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class); + return this.restOperations.exchange(request, OAuth2AccessTokenResponse.class); } catch (RestClientException ex) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, @@ -82,30 +99,15 @@ public final class DefaultJwtBearerTokenResponseClient null); throw new OAuth2AuthorizationException(oauth2Error, ex); } - - OAuth2AccessTokenResponse tokenResponse = response.getBody(); - - if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) { - // As per spec, in Section 5.1 Successful Access Token Response - // https://tools.ietf.org/html/rfc6749#section-5.1 - // If AccessTokenResponse.scope is empty, then default to the scope - // originally requested by the client in the Token Request - tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) - .scopes(jwtBearerGrantRequest.getClientRegistration().getScopes()).build(); - } - - return tokenResponse; } /** - * Sets the {@link Converter} used for converting the - * {@link OAuth2JwtBearerGrantRequest} to a {@link RequestEntity} representation of - * the OAuth 2.0 Access Token Request. + * Sets the {@link Converter} used for converting the {@link JwtBearerGrantRequest} to + * a {@link RequestEntity} representation of the OAuth 2.0 Access Token Request. * @param requestEntityConverter the {@link Converter} used for converting to a * {@link RequestEntity} representation of the Access Token Request */ - public void setRequestEntityConverter( - Converter> requestEntityConverter) { + public void setRequestEntityConverter(Converter> requestEntityConverter) { Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null"); this.requestEntityConverter = requestEntityConverter; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequest.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequest.java similarity index 56% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequest.java rename to oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequest.java index d5f39bbbb3..d5fed5d87a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequest.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequest.java @@ -22,53 +22,36 @@ import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.util.Assert; /** - * A JWT Bearer Authorization Grant request that holds a trusted {@link #getJwt() JWT} - * credential, which was granted by the Resource Owner to the - * {@link #getClientRegistration() Client}. + * A JWT Bearer Grant request that holds a {@link Jwt} assertion. * * @author Joe Grandja * @since 5.5 * @see AbstractOAuth2AuthorizationGrantRequest * @see ClientRegistration * @see Jwt - * @see Section - * 2.1 JWTs as Authorization Grants + * @see Section + * 2.1 Using JWTs as Authorization Grants */ -public class OAuth2JwtBearerGrantRequest extends AbstractOAuth2AuthorizationGrantRequest { - - public static final AuthorizationGrantType JWT_BEARER_GRANT_TYPE = new AuthorizationGrantType( - "urn:ietf:params:oauth:grant-type:jwt-bearer"); - - private final ClientRegistration clientRegistration; +public class JwtBearerGrantRequest extends AbstractOAuth2AuthorizationGrantRequest { private final Jwt jwt; /** - * Constructs an {@code OAuth2JwtBearerGrantRequest} using the provided parameters. + * Constructs a {@code JwtBearerGrantRequest} using the provided parameters. * @param clientRegistration the client registration - * @param jwt the JWT Bearer token + * @param jwt the JWT assertion */ - public OAuth2JwtBearerGrantRequest(ClientRegistration clientRegistration, Jwt jwt) { - super(JWT_BEARER_GRANT_TYPE); - Assert.notNull(clientRegistration, "clientRegistration cannot be null"); - Assert.notNull(jwt, "jwt cannot be null"); + public JwtBearerGrantRequest(ClientRegistration clientRegistration, Jwt jwt) { + super(AuthorizationGrantType.JWT_BEARER, clientRegistration); Assert.isTrue(AuthorizationGrantType.JWT_BEARER.equals(clientRegistration.getAuthorizationGrantType()), "clientRegistration.authorizationGrantType must be AuthorizationGrantType.JWT_BEARER"); - this.clientRegistration = clientRegistration; + Assert.notNull(jwt, "jwt cannot be null"); this.jwt = jwt; } /** - * Returns the {@link ClientRegistration client registration}. - * @return the {@link ClientRegistration} - */ - public ClientRegistration getClientRegistration() { - return this.clientRegistration; - } - - /** - * Returns the {@link Jwt JWT Bearer token}. - * @return the {@link Jwt} + * Returns the {@link Jwt JWT} assertion. + * @return the {@link Jwt} assertion */ public Jwt getJwt() { return this.jwt; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequestEntityConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequestEntityConverter.java new file mode 100644 index 0000000000..b0a8320363 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequestEntityConverter.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2021 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.endpoint; + +import org.springframework.http.RequestEntity; +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; + +/** + * An implementation of an {@link AbstractOAuth2AuthorizationGrantRequestEntityConverter} + * that converts the provided {@link JwtBearerGrantRequest} to a {@link RequestEntity} + * representation of an OAuth 2.0 Access Token Request for the JWT Bearer Grant. + * + * @author Joe Grandja + * @since 5.5 + * @see AbstractOAuth2AuthorizationGrantRequestEntityConverter + * @see JwtBearerGrantRequest + * @see RequestEntity + * @see Section + * 2.1 Using JWTs as Authorization Grants + */ +public class JwtBearerGrantRequestEntityConverter + extends AbstractOAuth2AuthorizationGrantRequestEntityConverter { + + @Override + protected MultiValueMap createParameters(JwtBearerGrantRequest jwtBearerGrantRequest) { + ClientRegistration clientRegistration = jwtBearerGrantRequest.getClientRegistration(); + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add(OAuth2ParameterNames.GRANT_TYPE, jwtBearerGrantRequest.getGrantType().getValue()); + parameters.add(OAuth2ParameterNames.ASSERTION, jwtBearerGrantRequest.getJwt().getTokenValue()); + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + parameters.add(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " ")); + } + if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod()) + || ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) { + parameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId()); + parameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); + } + return parameters; + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequestEntityConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequestEntityConverter.java deleted file mode 100644 index 637016976e..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequestEntityConverter.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2002-2021 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.client.endpoint; - -import java.net.URI; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.RequestEntity; -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; - -/** - * A {@link Converter} that converts the provided {@link OAuth2JwtBearerGrantRequest} to a - * {@link RequestEntity} representation of an OAuth 2.0 Access Token Request for the Jwt - * Bearer Grant. - * - * @author Joe Grandja - * @since 5.5 - * @see Converter - * @see OAuth2JwtBearerGrantRequest - * @see RequestEntity - * @see Section - * 2.1 JWTs as Authorization Grants - */ -public class OAuth2JwtBearerGrantRequestEntityConverter - implements Converter> { - - /** - * Returns the {@link RequestEntity} used for the Access Token Request. - * @param jwtBearerGrantRequest the Jwt Bearer grant request - * @return the {@link RequestEntity} used for the Access Token Request - */ - @Override - public RequestEntity convert(OAuth2JwtBearerGrantRequest jwtBearerGrantRequest) { - ClientRegistration clientRegistration = jwtBearerGrantRequest.getClientRegistration(); - - HttpHeaders headers = OAuth2AuthorizationGrantRequestEntityUtils.getTokenRequestHeaders(clientRegistration); - MultiValueMap formParameters = this.buildFormParameters(jwtBearerGrantRequest); - URI 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 jwtBearerGrantRequest the Jwt Bearer grant request - * @return a {@link MultiValueMap} of the form parameters used for the Access Token - * Request body - */ - private MultiValueMap buildFormParameters(OAuth2JwtBearerGrantRequest jwtBearerGrantRequest) { - ClientRegistration clientRegistration = jwtBearerGrantRequest.getClientRegistration(); - - MultiValueMap formParameters = new LinkedMultiValueMap<>(); - formParameters.add(OAuth2ParameterNames.GRANT_TYPE, jwtBearerGrantRequest.getGrantType().getValue()); - formParameters.add(OAuth2ParameterNames.ASSERTION, jwtBearerGrantRequest.getJwt().getTokenValue()); - if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { - formParameters.add(OAuth2ParameterNames.SCOPE, StringUtils - .collectionToDelimitedString(jwtBearerGrantRequest.getClientRegistration().getScopes(), " ")); - } - if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) { - formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId()); - formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); - } - - return formParameters; - } - -} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProviderTests.java index 5fcc9a4186..db7948fd6d 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProviderTests.java @@ -16,19 +16,18 @@ package org.springframework.security.oauth2.client; -import java.time.Duration; -import java.time.Instant; - import org.junit.Before; import org.junit.Test; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.OAuth2JwtBearerGrantRequest; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; -import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.TestOAuth2AccessTokens; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; import org.springframework.security.oauth2.jwt.Jwt; @@ -44,27 +43,37 @@ import static org.mockito.Mockito.mock; * Tests for {@link JwtBearerOAuth2AuthorizedClientProvider}. * * @author Hassene Laaribi + * @author Joe Grandja */ public class JwtBearerOAuth2AuthorizedClientProviderTests { private JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider; - private OAuth2AccessTokenResponseClient accessTokenResponseClient; + private OAuth2AccessTokenResponseClient accessTokenResponseClient; private ClientRegistration clientRegistration; - private Authentication principal; + private Jwt jwtAssertion; - private Jwt jwtBearerToken; + private Authentication principal; @Before public void setup() { this.authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider(); this.accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class); this.authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient); - this.clientRegistration = TestClientRegistrations.jwtBearer().build(); - this.jwtBearerToken = TestJwts.jwt().build(); - this.principal = new TestingAuthenticationToken("principal", this.jwtBearerToken); + // @formatter:off + this.clientRegistration = ClientRegistration.withRegistrationId("jwt-bearer") + .clientId("client-id") + .clientSecret("client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) + .scope("read", "write") + .tokenUri("https://example.com/oauth2/token") + .build(); + // @formatter:on + this.jwtAssertion = TestJwts.jwt().build(); + this.principal = new TestingAuthenticationToken(this.jwtAssertion, this.jwtAssertion); } @Test @@ -74,33 +83,6 @@ public class JwtBearerOAuth2AuthorizedClientProviderTests { .withMessage("accessTokenResponseClient cannot be null"); } - @Test - public void setClockSkewWhenNullThenThrowIllegalArgumentException() { - // @formatter:off - assertThatIllegalArgumentException() - .isThrownBy(() -> this.authorizedClientProvider.setClockSkew(null)) - .withMessage("clockSkew cannot be null"); - // @formatter:on - } - - @Test - public void setClockSkewWhenNegativeSecondsThenThrowIllegalArgumentException() { - // @formatter:off - assertThatIllegalArgumentException() - .isThrownBy(() -> this.authorizedClientProvider.setClockSkew(Duration.ofSeconds(-1))) - .withMessage("clockSkew must be >= 0"); - // @formatter:on - } - - @Test - public void setClockWhenNullThenThrowIllegalArgumentException() { - // @formatter:off - assertThatIllegalArgumentException() - .isThrownBy(() -> this.authorizedClientProvider.setClock(null)) - .withMessage("clock cannot be null"); - // @formatter:on - } - @Test public void authorizeWhenContextIsNullThenThrowIllegalArgumentException() { // @formatter:off @@ -123,26 +105,37 @@ public class JwtBearerOAuth2AuthorizedClientProviderTests { } @Test - public void authorizeWhenJwtBearerAndNotAuthorizedAndEmptyJwtThenUnableToAuthorize() { + public void authorizeWhenJwtBearerAndAuthorizedThenNotAuthorized() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, + this.principal.getName(), TestOAuth2AccessTokens.scopes("read", "write")); // @formatter:off OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext - .withClientRegistration(this.clientRegistration) + .withAuthorizedClient(authorizedClient) .principal(this.principal) - .attribute(OAuth2AuthorizationContext.JWT_ATTRIBUTE_NAME, null) .build(); // @formatter:on assertThat(this.authorizedClientProvider.authorize(authorizationContext)).isNull(); } @Test - public void authorizeWhenJwtBearerAndNotAuthorizedThenAuthorize() { + public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalNotJwtThenUnableToAuthorize() { + // @formatter:off + OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext + .withClientRegistration(this.clientRegistration) + .principal(new TestingAuthenticationToken("user", "password")) + .build(); + // @formatter:on + assertThat(this.authorizedClientProvider.authorize(authorizationContext)).isNull(); + } + + @Test + public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalJwtThenAuthorize() { OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(accessTokenResponse); // @formatter:off OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext .withClientRegistration(this.clientRegistration) .principal(this.principal) - .attribute(OAuth2AuthorizationContext.JWT_ATTRIBUTE_NAME, this.jwtBearerToken) .build(); // @formatter:on OAuth2AuthorizedClient authorizedClient = this.authorizedClientProvider.authorize(authorizationContext); @@ -151,55 +144,4 @@ public class JwtBearerOAuth2AuthorizedClientProviderTests { assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); } - @Test - public void authorizeWhenJwtBearerAndAuthorizedAndTokenExpiredThenReauthorize() { - Instant issuedAt = Instant.now().minus(Duration.ofDays(1)); - Instant expiresAt = issuedAt.plus(Duration.ofMinutes(60)); - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - "access-token-expired", issuedAt, expiresAt); - OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, - this.principal.getName(), accessToken); - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); - given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(accessTokenResponse); - // @formatter:off - OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext - .withAuthorizedClient(authorizedClient) - .attribute(OAuth2AuthorizationContext.JWT_ATTRIBUTE_NAME, this.jwtBearerToken) - .principal(this.principal) - .build(); - // @formatter:on - authorizedClient = this.authorizedClientProvider.authorize(authorizationContext); - assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); - assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principal.getName()); - assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); - } - - @Test - public void authorizeWhenJwtBearerAndAuthorizedAndTokenNotExpiredButClockSkewForcesExpiryThenReauthorize() { - Instant now = Instant.now(); - Instant issuedAt = now.minus(Duration.ofMinutes(60)); - Instant expiresAt = now.plus(Duration.ofMinutes(1)); - OAuth2AccessToken expiresInOneMinAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - "access-token-1234", issuedAt, expiresAt); - OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, - this.principal.getName(), expiresInOneMinAccessToken); // without refresh - // token - // Shorten the lifespan of the access token by 90 seconds, which will ultimately - // force it to expire on the client - this.authorizedClientProvider.setClockSkew(Duration.ofSeconds(90)); - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); - given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(accessTokenResponse); - // @formatter:off - OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext - .withAuthorizedClient(authorizedClient) - .attribute(OAuth2AuthorizationContext.JWT_ATTRIBUTE_NAME, this.jwtBearerToken) - .principal(this.principal) - .build(); - // @formatter:on - OAuth2AuthorizedClient reauthorizedClient = this.authorizedClientProvider.authorize(authorizationContext); - assertThat(reauthorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); - assertThat(reauthorizedClient.getPrincipalName()).isEqualTo(this.principal.getName()); - assertThat(reauthorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); - } - } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilderTests.java index 72889e98c3..3275de9195 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilderTests.java @@ -28,7 +28,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.DefaultJwtBearerTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -37,7 +36,6 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; -import org.springframework.security.oauth2.jwt.TestJwts; import org.springframework.web.client.RestOperations; import static org.assertj.core.api.Assertions.assertThat; @@ -65,8 +63,6 @@ public class OAuth2AuthorizedClientProviderBuilderTests { private DefaultPasswordTokenResponseClient passwordTokenResponseClient; - private DefaultJwtBearerTokenResponseClient jwtBearerTokenResponseClient; - private Authentication principal; @SuppressWarnings("unchecked") @@ -82,8 +78,6 @@ public class OAuth2AuthorizedClientProviderBuilderTests { this.clientCredentialsTokenResponseClient.setRestOperations(this.accessTokenClient); this.passwordTokenResponseClient = new DefaultPasswordTokenResponseClient(); this.passwordTokenResponseClient.setRestOperations(this.accessTokenClient); - this.jwtBearerTokenResponseClient = new DefaultJwtBearerTokenResponseClient(); - this.jwtBearerTokenResponseClient.setRestOperations(this.accessTokenClient); this.principal = new TestingAuthenticationToken("principal", "password"); } @@ -163,23 +157,6 @@ public class OAuth2AuthorizedClientProviderBuilderTests { verify(this.accessTokenClient).exchange(any(RequestEntity.class), eq(OAuth2AccessTokenResponse.class)); } - @Test - public void buildWhenJwtBearerProviderThenProviderAuthorizes() { - // @formatter:off - OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .jwtBearer((configurer) -> configurer.accessTokenResponseClient(this.jwtBearerTokenResponseClient)) - .build(); - OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext - .withClientRegistration(TestClientRegistrations.jwtBearer().build()) - .principal(this.principal) - .attribute(OAuth2AuthorizationContext.JWT_ATTRIBUTE_NAME, TestJwts.jwt().build()) - .build(); - // @formatter:on - OAuth2AuthorizedClient authorizedClient = authorizedClientProvider.authorize(authorizationContext); - assertThat(authorizedClient).isNotNull(); - verify(this.accessTokenClient).exchange(any(RequestEntity.class), eq(OAuth2AccessTokenResponse.class)); - } - @Test public void buildWhenAllProvidersThenProvidersAuthorize() { OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() @@ -189,7 +166,6 @@ public class OAuth2AuthorizedClientProviderBuilderTests { .clientCredentials( (configurer) -> configurer.accessTokenResponseClient(this.clientCredentialsTokenResponseClient)) .password((configurer) -> configurer.accessTokenResponseClient(this.passwordTokenResponseClient)) - .jwtBearer((configurer) -> configurer.accessTokenResponseClient(this.jwtBearerTokenResponseClient)) .build(); ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build(); // authorization_code diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClientTests.java index 4b69fc4342..1077bc08d4 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClientTests.java @@ -32,6 +32,7 @@ import org.springframework.http.MediaType; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; @@ -46,26 +47,33 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException * Tests for {@link DefaultJwtBearerTokenResponseClient}. * * @author Hassene Laaribi + * @author Joe Grandja */ public class DefaultJwtBearerTokenResponseClientTests { - private static final String UTF_8_CHARSET = ";charset=UTF-8"; + private DefaultJwtBearerTokenResponseClient tokenResponseClient; - private final DefaultJwtBearerTokenResponseClient tokenResponseClient = new DefaultJwtBearerTokenResponseClient(); + private ClientRegistration.Builder clientRegistration; - private ClientRegistration.Builder clientRegistrationBuilder; - - private final Jwt jwtBearerToken = TestJwts.jwt().build(); + private Jwt jwtAssertion; private MockWebServer server; @Before public void setup() throws Exception { + this.tokenResponseClient = new DefaultJwtBearerTokenResponseClient(); this.server = new MockWebServer(); this.server.start(); String tokenUri = this.server.url("/oauth2/token").toString(); - this.clientRegistrationBuilder = TestClientRegistrations.clientRegistration() - .authorizationGrantType(AuthorizationGrantType.JWT_BEARER).scope("read", "write").tokenUri(tokenUri); + // @formatter:off + this.clientRegistration = TestClientRegistrations.clientCredentials() + .clientId("client-1") + .clientSecret("secret") + .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) + .tokenUri(tokenUri) + .scope("read", "write"); + // @formatter:on + this.jwtAssertion = TestJwts.jwt().build(); } @After @@ -99,18 +107,16 @@ public class DefaultJwtBearerTokenResponseClientTests { // @formatter:on this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); Instant expiresAtBefore = Instant.now().plusSeconds(3600); - ClientRegistration clientRegistration = this.clientRegistrationBuilder.build(); - OAuth2JwtBearerGrantRequest jwtBearerGrantRequest = new OAuth2JwtBearerGrantRequest(clientRegistration, - this.jwtBearerToken); + ClientRegistration clientRegistration = this.clientRegistration.build(); + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(clientRegistration, this.jwtAssertion); OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient .getTokenResponse(jwtBearerGrantRequest); Instant expiresAtAfter = Instant.now().plusSeconds(3600); RecordedRequest recordedRequest = this.server.takeRequest(); assertThat(recordedRequest.getMethod()).isEqualTo(HttpMethod.POST.toString()); - assertThat(recordedRequest.getHeader(HttpHeaders.ACCEPT)) - .isEqualTo(MediaType.APPLICATION_JSON_VALUE + UTF_8_CHARSET); + assertThat(recordedRequest.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE); assertThat(recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE)) - .isEqualTo(MediaType.APPLICATION_FORM_URLENCODED_VALUE + UTF_8_CHARSET); + .isEqualTo(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"); String formParameters = recordedRequest.getBody().readUtf8(); assertThat(formParameters) .contains("grant_type=" + URLEncoder.encode(AuthorizationGrantType.JWT_BEARER.getValue(), "UTF-8")); @@ -118,11 +124,48 @@ public class DefaultJwtBearerTokenResponseClientTests { assertThat(accessTokenResponse.getAccessToken().getTokenValue()).isEqualTo("access-token-1234"); assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBetween(expiresAtBefore, expiresAtAfter); - assertThat(accessTokenResponse.getAccessToken().getScopes()) - .containsExactly(clientRegistration.getScopes().toArray(new String[0])); + assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactlyInAnyOrder("read", "write"); assertThat(accessTokenResponse.getRefreshToken()).isNull(); } + @Test + public void getTokenResponseWhenAuthenticationClientSecretBasicThenAuthorizationHeaderIsSent() throws Exception { + // @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)); + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(this.clientRegistration.build(), + this.jwtAssertion); + this.tokenResponseClient.getTokenResponse(jwtBearerGrantRequest); + RecordedRequest recordedRequest = this.server.takeRequest(); + assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).startsWith("Basic "); + } + + @Test + public void getTokenResponseWhenAuthenticationClientSecretPostThenFormParametersAreSent() throws Exception { + // @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)); + ClientRegistration clientRegistration = this.clientRegistration + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST).build(); + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(clientRegistration, this.jwtAssertion); + this.tokenResponseClient.getTokenResponse(jwtBearerGrantRequest); + RecordedRequest recordedRequest = this.server.takeRequest(); + assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull(); + String formParameters = recordedRequest.getBody().readUtf8(); + assertThat(formParameters).contains("client_id=client-1"); + assertThat(formParameters).contains("client_secret=secret"); + } + @Test public void getTokenResponseWhenSuccessResponseAndNotBearerTokenTypeThenThrowOAuth2AuthorizationException() { // @formatter:off @@ -133,9 +176,8 @@ public class DefaultJwtBearerTokenResponseClientTests { + "}\n"; // @formatter:on this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); - ClientRegistration clientRegistration = this.clientRegistrationBuilder.build(); - OAuth2JwtBearerGrantRequest jwtBearerGrantRequest = new OAuth2JwtBearerGrantRequest(clientRegistration, - this.jwtBearerToken); + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(this.clientRegistration.build(), + this.jwtAssertion); assertThatExceptionOfType(OAuth2AuthorizationException.class) .isThrownBy(() -> this.tokenResponseClient.getTokenResponse(jwtBearerGrantRequest)) .withMessageContaining( @@ -154,35 +196,46 @@ public class DefaultJwtBearerTokenResponseClientTests { + "}\n"; // @formatter:on this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); - ClientRegistration clientRegistration = this.clientRegistrationBuilder.build(); - OAuth2JwtBearerGrantRequest jwtBearerGrantRequest = new OAuth2JwtBearerGrantRequest(clientRegistration, - this.jwtBearerToken); + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(this.clientRegistration.build(), + this.jwtAssertion); OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient .getTokenResponse(jwtBearerGrantRequest); - RecordedRequest recordedRequest = this.server.takeRequest(); - String formParameters = recordedRequest.getBody().readUtf8(); - assertThat(formParameters).contains("scope=read"); assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read"); } + @Test + public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenAccessTokenHasDefaultScope() { + // @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)); + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(this.clientRegistration.build(), + this.jwtAssertion); + OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient + .getTokenResponse(jwtBearerGrantRequest); + assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write"); + } + @Test public void getTokenResponseWhenErrorResponseThenThrowOAuth2AuthorizationException() { - String accessTokenErrorResponse = "{\n" + " \"error\": \"unauthorized_client\"\n" + "}\n"; + String accessTokenErrorResponse = "{\n" + " \"error\": \"invalid_grant\"\n" + "}\n"; this.server.enqueue(jsonResponse(accessTokenErrorResponse).setResponseCode(400)); - ClientRegistration clientRegistration = this.clientRegistrationBuilder.build(); - OAuth2JwtBearerGrantRequest jwtBearerGrantRequest = new OAuth2JwtBearerGrantRequest(clientRegistration, - this.jwtBearerToken); + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(this.clientRegistration.build(), + this.jwtAssertion); assertThatExceptionOfType(OAuth2AuthorizationException.class) .isThrownBy(() -> this.tokenResponseClient.getTokenResponse(jwtBearerGrantRequest)) - .withMessageContaining("[unauthorized_client]"); + .withMessageContaining("[invalid_grant]"); } @Test public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthorizationException() { this.server.enqueue(new MockResponse().setResponseCode(500)); - ClientRegistration clientRegistration = this.clientRegistrationBuilder.build(); - OAuth2JwtBearerGrantRequest jwtBearerGrantRequest = new OAuth2JwtBearerGrantRequest(clientRegistration, - this.jwtBearerToken); + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(this.clientRegistration.build(), + this.jwtAssertion); assertThatExceptionOfType(OAuth2AuthorizationException.class) .isThrownBy(() -> this.tokenResponseClient.getTokenResponse(jwtBearerGrantRequest)) .withMessageContaining("[invalid_token_response] An error occurred while attempting to " diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequestEntityConverterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequestEntityConverterTests.java new file mode 100644 index 0000000000..cca7638c7f --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequestEntityConverterTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2021 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.endpoint; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +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.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.TestClientRegistrations; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.TestJwts; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JwtBearerGrantRequestEntityConverter}. + * + * @author Hassene Laaribi + * @author Joe Grandja + */ +public class JwtBearerGrantRequestEntityConverterTests { + + private JwtBearerGrantRequestEntityConverter converter; + + @Before + public void setup() { + this.converter = new JwtBearerGrantRequestEntityConverter(); + } + + @Test + public void setHeadersConverterWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.converter.setHeadersConverter(null)) + .withMessage("headersConverter cannot be null"); + } + + @Test + public void addHeadersConverterWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.converter.addHeadersConverter(null)) + .withMessage("headersConverter cannot be null"); + } + + @Test + public void setParametersConverterWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.converter.setParametersConverter(null)) + .withMessage("parametersConverter cannot be null"); + } + + @Test + public void addParametersConverterWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.converter.addParametersConverter(null)) + .withMessage("parametersConverter cannot be null"); + } + + @Test + public void convertWhenHeadersConverterSetThenCalled() { + Converter headersConverter1 = mock(Converter.class); + this.converter.setHeadersConverter(headersConverter1); + Converter headersConverter2 = mock(Converter.class); + this.converter.addHeadersConverter(headersConverter2); + // @formatter:off + ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration() + .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) + .scope("read", "write") + .build(); + // @formatter:on + Jwt jwtAssertion = TestJwts.jwt().build(); + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(clientRegistration, jwtAssertion); + this.converter.convert(jwtBearerGrantRequest); + InOrder inOrder = inOrder(headersConverter1, headersConverter2); + inOrder.verify(headersConverter1).convert(any(JwtBearerGrantRequest.class)); + inOrder.verify(headersConverter2).convert(any(JwtBearerGrantRequest.class)); + } + + @Test + public void convertWhenParametersConverterSetThenCalled() { + Converter> parametersConverter1 = mock(Converter.class); + this.converter.setParametersConverter(parametersConverter1); + Converter> parametersConverter2 = mock(Converter.class); + this.converter.addParametersConverter(parametersConverter2); + // @formatter:off + ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration() + .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) + .scope("read", "write") + .build(); + // @formatter:on + Jwt jwtAssertion = TestJwts.jwt().build(); + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(clientRegistration, jwtAssertion); + this.converter.convert(jwtBearerGrantRequest); + InOrder inOrder = inOrder(parametersConverter1, parametersConverter2); + inOrder.verify(parametersConverter1).convert(any(JwtBearerGrantRequest.class)); + inOrder.verify(parametersConverter2).convert(any(JwtBearerGrantRequest.class)); + } + + @SuppressWarnings("unchecked") + @Test + public void convertWhenGrantRequestValidThenConverts() { + // @formatter:off + ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration() + .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) + .scope("read", "write") + .build(); + // @formatter:on + Jwt jwtAssertion = TestJwts.jwt().build(); + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(clientRegistration, jwtAssertion); + RequestEntity requestEntity = this.converter.convert(jwtBearerGrantRequest); + assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(requestEntity.getUrl().toASCIIString()) + .isEqualTo(clientRegistration.getProviderDetails().getTokenUri()); + HttpHeaders headers = requestEntity.getHeaders(); + assertThat(headers.getAccept()).contains(MediaType.valueOf(MediaType.APPLICATION_JSON_UTF8_VALUE)); + assertThat(headers.getContentType()) + .isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8")); + assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).startsWith("Basic "); + MultiValueMap formParameters = (MultiValueMap) requestEntity.getBody(); + assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE)) + .isEqualTo(AuthorizationGrantType.JWT_BEARER.getValue()); + assertThat(formParameters.getFirst(OAuth2ParameterNames.ASSERTION)).isEqualTo(jwtAssertion.getTokenValue()); + assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).isEqualTo("read write"); + } + +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequestTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequestTests.java similarity index 76% rename from oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequestTests.java rename to oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequestTests.java index a02fba3c47..aca31d5fe4 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequestTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/JwtBearerGrantRequestTests.java @@ -28,28 +28,26 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Tests for {@link OAuth2JwtBearerGrantRequest}. + * Tests for {@link JwtBearerGrantRequest}. * * @author Hassene Laaribi */ -public class OAuth2JwtBearerGrantRequestTests { +public class JwtBearerGrantRequestTests { private final ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration() .authorizationGrantType(AuthorizationGrantType.JWT_BEARER).build(); - private final Jwt jwtBearerToken = TestJwts.jwt().build(); + private final Jwt jwtAssertion = TestJwts.jwt().build(); @Test public void constructorWhenClientRegistrationIsNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new OAuth2JwtBearerGrantRequest(null, this.jwtBearerToken)) + assertThatIllegalArgumentException().isThrownBy(() -> new JwtBearerGrantRequest(null, this.jwtAssertion)) .withMessage("clientRegistration cannot be null"); } @Test public void constructorWhenJwtIsNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new OAuth2JwtBearerGrantRequest(this.clientRegistration, null)) + assertThatIllegalArgumentException().isThrownBy(() -> new JwtBearerGrantRequest(this.clientRegistration, null)) .withMessage("jwt cannot be null"); } @@ -57,17 +55,17 @@ public class OAuth2JwtBearerGrantRequestTests { public void constructorWhenClientRegistrationInvalidGrantTypeThenThrowIllegalArgumentException() { ClientRegistration registration = TestClientRegistrations.clientCredentials().build(); assertThatIllegalArgumentException() - .isThrownBy(() -> new OAuth2JwtBearerGrantRequest(registration, this.jwtBearerToken)) + .isThrownBy(() -> new JwtBearerGrantRequest(registration, this.jwtAssertion)) .withMessage("clientRegistration.authorizationGrantType must be AuthorizationGrantType.JWT_BEARER"); } @Test public void constructorWhenValidParametersProvidedThenCreated() { - OAuth2JwtBearerGrantRequest jwtBearerGrantRequest = new OAuth2JwtBearerGrantRequest(this.clientRegistration, - this.jwtBearerToken); + JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(this.clientRegistration, + this.jwtAssertion); assertThat(jwtBearerGrantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER); assertThat(jwtBearerGrantRequest.getClientRegistration()).isSameAs(this.clientRegistration); - assertThat(jwtBearerGrantRequest.getJwt()).isEqualTo(this.jwtBearerToken); + assertThat(jwtBearerGrantRequest.getJwt()).isSameAs(this.jwtAssertion); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequestEntityConverterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequestEntityConverterTests.java deleted file mode 100644 index a0f4006680..0000000000 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2JwtBearerGrantRequestEntityConverterTests.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2002-2021 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.client.endpoint; - -import org.junit.Before; -import org.junit.Test; - -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.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.TestClientRegistrations; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.TestJwts; -import org.springframework.util.MultiValueMap; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link OAuth2JwtBearerGrantRequestEntityConverter}. - * - * @author Hassene Laaribi - */ -public class OAuth2JwtBearerGrantRequestEntityConverterTests { - - private static final String UTF_8_CHARSET = ";charset=UTF-8"; - - private final OAuth2JwtBearerGrantRequestEntityConverter converter = new OAuth2JwtBearerGrantRequestEntityConverter(); - - private OAuth2JwtBearerGrantRequest jwtBearerGrantRequest; - - private final Jwt jwtBearerToken = TestJwts.jwt().build(); - - @Before - public void setup() { - // @formatter:off - ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration() - .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) - .scope("read", "write") - .build(); - // @formatter:on - this.jwtBearerGrantRequest = new OAuth2JwtBearerGrantRequest(clientRegistration, this.jwtBearerToken); - } - - @Test - public void convertWhenGrantRequestValidThenConverts() { - RequestEntity requestEntity = this.converter.convert(this.jwtBearerGrantRequest); - ClientRegistration clientRegistration = this.jwtBearerGrantRequest.getClientRegistration(); - assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.POST); - assertThat(requestEntity.getUrl().toASCIIString()) - .isEqualTo(clientRegistration.getProviderDetails().getTokenUri()); - HttpHeaders headers = requestEntity.getHeaders(); - assertThat(headers.getAccept()).contains(MediaType.valueOf(MediaType.APPLICATION_JSON_VALUE + UTF_8_CHARSET)); - assertThat(headers.getContentType()) - .isEqualTo(MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + UTF_8_CHARSET)); - assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).startsWith("Basic "); - MultiValueMap formParameters = (MultiValueMap) requestEntity.getBody(); - assertThat(formParameters.getFirst(OAuth2ParameterNames.GRANT_TYPE)) - .isEqualTo(AuthorizationGrantType.JWT_BEARER.getValue()); - assertThat(formParameters.getFirst(OAuth2ParameterNames.ASSERTION)) - .isEqualTo(this.jwtBearerToken.getTokenValue()); - assertThat(formParameters.getFirst(OAuth2ParameterNames.SCOPE)).isEqualTo("read write"); - } - -} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java index 931c863887..e2ef5a8bb1 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java @@ -86,17 +86,4 @@ public final class TestClientRegistrations { // @formatter:on } - public static ClientRegistration.Builder jwtBearer() { - // @formatter:off - return ClientRegistration.withRegistrationId("jwt-bearer") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) - .scope("read", "write") - .tokenUri("https://example.com/login/oauth/access_token") - .clientName("Client Name") - .clientId("client-id") - .clientSecret("client-secret"); - // @formatter:on - } - } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java index df70cf5aba..05b1d8422f 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -66,6 +66,7 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.ClientAuthorizationException; +import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.OAuth2AuthorizationContext; import org.springframework.security.oauth2.client.OAuth2AuthorizationFailureHandler; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; @@ -75,9 +76,9 @@ import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedCl import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2JwtBearerGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -85,6 +86,8 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -94,6 +97,8 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenRespon import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.TestJwts; import org.springframework.util.StringUtils; import org.springframework.web.client.RestOperations; import org.springframework.web.context.request.RequestContextHolder; @@ -141,7 +146,7 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests { private OAuth2AccessTokenResponseClient passwordTokenResponseClient; @Mock - private OAuth2AccessTokenResponseClient jwtBearerTokenResponseClient; + private OAuth2AccessTokenResponseClient jwtBearerTokenResponseClient; @Mock private OAuth2AuthorizationFailureHandler authorizationFailureHandler; @@ -185,6 +190,9 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests { @Before public void setup() { this.authentication = new TestingAuthenticationToken("test", "this"); + JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider(); + jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(this.jwtBearerTokenResponseClient); + // @formatter:off OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .authorizationCode() .refreshToken( @@ -192,8 +200,9 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests { .clientCredentials( (configurer) -> configurer.accessTokenResponseClient(this.clientCredentialsTokenResponseClient)) .password((configurer) -> configurer.accessTokenResponseClient(this.passwordTokenResponseClient)) - .jwtBearer((configurer) -> configurer.accessTokenResponseClient(this.jwtBearerTokenResponseClient)) + .provider(jwtBearerAuthorizedClientProvider) .build(); + // @formatter:on this.authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(this.clientRegistrationRepository, this.authorizedClientRepository); this.authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); @@ -492,7 +501,7 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests { servletRequest.setParameter(OAuth2ParameterNames.PASSWORD, "password"); MockHttpServletResponse servletResponse = new MockHttpServletResponse(); ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("https://example.com")) - .attributes(ServerOAuth2AuthorizedClientExchangeFilterFunction + .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction .clientRegistrationId(registration.getRegistrationId())) .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.authentication(this.authentication)) .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.httpServletRequest(servletRequest)) @@ -510,6 +519,46 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests { assertThat(getBody(request1)).isEmpty(); } + @Test + public void filterWhenJwtBearerClientNotAuthorizedThenExchangeToken() { + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("exchanged-token") + .tokenType(OAuth2AccessToken.TokenType.BEARER).expiresIn(360).build(); + given(this.jwtBearerTokenResponseClient.getTokenResponse(any())).willReturn(accessTokenResponse); + // @formatter:off + ClientRegistration registration = ClientRegistration.withRegistrationId("jwt-bearer") + .clientId("client-id") + .clientSecret("client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) + .scope("read", "write") + .tokenUri("https://example.com/oauth/token") + .build(); + // @formatter:on + given(this.clientRegistrationRepository.findByRegistrationId(eq(registration.getRegistrationId()))) + .willReturn(registration); + Jwt jwtAssertion = TestJwts.jwt().build(); + Authentication jwtAuthentication = new TestingAuthenticationToken(jwtAssertion, jwtAssertion); + MockHttpServletRequest servletRequest = new MockHttpServletRequest(); + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("https://example.com")) + .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction + .clientRegistrationId(registration.getRegistrationId())) + .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.authentication(jwtAuthentication)) + .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.httpServletRequest(servletRequest)) + .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.httpServletResponse(servletResponse)) + .build(); + this.function.filter(request, this.exchange).block(); + verify(this.jwtBearerTokenResponseClient).getTokenResponse(any()); + verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(jwtAuthentication), any(), any()); + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(1); + ClientRequest request1 = requests.get(0); + assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer exchanged-token"); + assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request1.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request1)).isEmpty(); + } + @Test public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() { OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1") diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java index 4b1154f2ab..755b53822b 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java @@ -59,6 +59,9 @@ public final class AuthorizationGrantType implements Serializable { public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password"); + /** + * @since 5.5 + */ public static final AuthorizationGrantType JWT_BEARER = new AuthorizationGrantType( "urn:ietf:params:oauth:grant-type:jwt-bearer"); diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java index 55b886f106..c1df365857 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java @@ -60,6 +60,12 @@ public interface OAuth2ParameterNames { */ String CLIENT_ASSERTION = "client_assertion"; + /** + * {@code assertion} - used in Access Token Request. + * @since 5.5 + */ + String ASSERTION = "assertion"; + /** * {@code redirect_uri} - used in Authorization Request and Access Token Request. */ @@ -111,11 +117,6 @@ public interface OAuth2ParameterNames { */ String PASSWORD = "password"; - /** - * {@code assertion} - used in Access Token Request. - */ - String ASSERTION = "assertion"; - /** * {@code error} - used in Authorization Response and Access Token Response. */ diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java index 4aae548f07..57fc6d05a1 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. @@ -53,4 +53,10 @@ public class AuthorizationGrantTypeTests { assertThat(AuthorizationGrantType.PASSWORD.getValue()).isEqualTo("password"); } + @Test + public void getValueWhenJwtBearerGrantTypeThenReturnJwtBearer() { + assertThat(AuthorizationGrantType.JWT_BEARER.getValue()) + .isEqualTo("urn:ietf:params:oauth:grant-type:jwt-bearer"); + } + }