diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfiguration.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfiguration.java index a91bc88..1a79cdc 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfiguration.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfiguration.java @@ -15,6 +15,7 @@ */ package org.springframework.security.oauth2.core.oidc; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; import org.springframework.security.oauth2.server.authorization.Version; import org.springframework.util.Assert; @@ -241,6 +242,30 @@ public final class OidcProviderConfiguration implements OidcProviderMetadataClai return this; } + /** + * Add this {@link JwsAlgorithm JWS} signing algorithm to the collection of {@code id_token_signing_alg_values_supported} + * in the resulting {@link OidcProviderConfiguration}, REQUIRED. + * + * @param signingAlgorithm the {@link JwsAlgorithm JWS} signing algorithm supported for the {@link OidcIdToken ID Token} + * @return the {@link Builder} for further configuration + */ + public Builder idTokenSigningAlgorithm(String signingAlgorithm) { + addClaimToClaimList(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, signingAlgorithm); + return this; + } + + /** + * A {@code Consumer} of the {@link JwsAlgorithm JWS} signing algorithms for the {@link OidcIdToken ID Token} + * allowing the ability to add, replace, or remove. + * + * @param signingAlgorithmsConsumer a {@code Consumer} of the {@link JwsAlgorithm JWS} signing algorithms for the {@link OidcIdToken ID Token} + * @return the {@link Builder} for further configuration + */ + public Builder idTokenSigningAlgorithms(Consumer> signingAlgorithmsConsumer) { + acceptClaimValues(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, signingAlgorithmsConsumer); + return this; + } + /** * Use this claim in the resulting {@link OidcProviderConfiguration}. * @@ -296,6 +321,9 @@ public final class OidcProviderConfiguration implements OidcProviderMetadataClai Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be null"); Assert.isInstanceOf(List.class, this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes must be of type List"); Assert.notEmpty((List) this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be empty"); + Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be null"); + Assert.isInstanceOf(List.class, this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms must be of type List"); + Assert.notEmpty((List) this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be empty"); } private static void validateURL(Object url, String errorMessage) { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimAccessor.java index 8aecf37..ce23226 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimAccessor.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimAccessor.java @@ -17,6 +17,8 @@ package org.springframework.security.oauth2.core.oidc; import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; import java.net.URL; import java.util.List; @@ -115,4 +117,14 @@ public interface OidcProviderMetadataClaimAccessor extends ClaimAccessor { return getClaimAsStringList(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED); } + /** + * Returns the {@link JwsAlgorithm JWS} signing algorithms supported for the {@link OidcIdToken ID Token} + * to encode the claims in a {@link Jwt} {@code (id_token_signing_alg_values_supported)}. + * + * @return the {@link JwsAlgorithm JWS} signing algorithms supported for the {@link OidcIdToken ID Token} + */ + default List getIdTokenSigningAlgorithms() { + return getClaimAsStringList(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED); + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimNames.java index b906484..046dc5c 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimNames.java @@ -15,6 +15,8 @@ */ package org.springframework.security.oauth2.core.oidc; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; + /** * The names of the "claims" defined by OpenID Connect Discovery 1.0 that can be returned * in the OpenID Provider Configuration Response. @@ -70,4 +72,9 @@ public interface OidcProviderMetadataClaimNames { */ String SCOPES_SUPPORTED = "scopes_supported"; + /** + * {@code id_token_signing_alg_values_supported} - the {@link JwsAlgorithm JWS} signing algorithms supported for the {@link OidcIdToken ID Token} + */ + String ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = "id_token_signing_alg_values_supported"; + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverter.java index df068f2..1e6cc14 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverter.java @@ -143,6 +143,7 @@ public class OidcProviderConfigurationHttpMessageConverter claimConverters.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, collectionStringConverter); claimConverters.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, collectionStringConverter); claimConverters.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, collectionStringConverter); + claimConverters.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, collectionStringConverter); claimConverters.put(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, collectionStringConverter); this.claimTypeConverter = new ClaimTypeConverter(claimConverters); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationToken.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationToken.java index 7854af0..86aab5c 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationToken.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationToken.java @@ -25,6 +25,7 @@ import org.springframework.security.oauth2.server.authorization.client.Registere import org.springframework.util.Assert; import java.util.Collections; +import java.util.Map; /** * An {@link Authentication} implementation used when issuing an @@ -45,6 +46,7 @@ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthentication private final Authentication clientPrincipal; private final OAuth2AccessToken accessToken; private final OAuth2RefreshToken refreshToken; + private final Map additionalParameters; /** * Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided parameters. @@ -68,14 +70,30 @@ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthentication */ public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient, Authentication clientPrincipal, OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) { + this(registeredClient, clientPrincipal, accessToken, refreshToken, Collections.emptyMap()); + } + + /** + * Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided parameters. + * + * @param registeredClient the registered client + * @param clientPrincipal the authenticated client principal + * @param accessToken the access token + * @param refreshToken the refresh token + * @param additionalParameters the additional parameters + */ + public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient, Authentication clientPrincipal, + OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken, Map additionalParameters) { super(Collections.emptyList()); Assert.notNull(registeredClient, "registeredClient cannot be null"); Assert.notNull(clientPrincipal, "clientPrincipal cannot be null"); Assert.notNull(accessToken, "accessToken cannot be null"); + Assert.notNull(additionalParameters, "additionalParameters cannot be null"); this.registeredClient = registeredClient; this.clientPrincipal = clientPrincipal; this.accessToken = accessToken; this.refreshToken = refreshToken; + this.additionalParameters = additionalParameters; } @Override @@ -115,4 +133,13 @@ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthentication public OAuth2RefreshToken getRefreshToken() { return this.refreshToken; } + + /** + * Returns the additional parameters. + * + * @return a {@code Map} of the additional parameters, may be empty + */ + public Map getAdditionalParameters() { + return this.additionalParameters; + } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java index dfac268..fc4a50a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -25,6 +25,9 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; @@ -39,6 +42,9 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; @@ -118,8 +124,9 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica } Set authorizedScopes = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZED_SCOPES); - Jwt jwt = OAuth2TokenIssuerUtil - .issueJwtAccessToken(this.jwtEncoder, authorization.getPrincipalName(), registeredClient.getClientId(), authorizedScopes, registeredClient.getTokenSettings().accessTokenTimeToLive()); + Jwt jwt = OAuth2TokenIssuerUtil.issueJwtAccessToken( + this.jwtEncoder, authorization.getPrincipalName(), registeredClient.getClientId(), + authorizedScopes, registeredClient.getTokenSettings().accessTokenTimeToLive()); OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), authorizedScopes); @@ -132,6 +139,16 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica tokensBuilder.refreshToken(refreshToken); } + OidcIdToken idToken = null; + if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) { + Jwt jwtIdToken = OAuth2TokenIssuerUtil.issueIdToken( + this.jwtEncoder, authorization.getPrincipalName(), registeredClient.getClientId(), + (String) authorizationRequest.getAdditionalParameters().get(OidcParameterNames.NONCE)); + idToken = new OidcIdToken(jwtIdToken.getTokenValue(), jwtIdToken.getIssuedAt(), + jwtIdToken.getExpiresAt(), jwtIdToken.getClaims()); + tokensBuilder.token(idToken); + } + OAuth2Tokens tokens = tokensBuilder.build(); authorization = OAuth2Authorization.from(authorization) .tokens(tokens) @@ -143,7 +160,14 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica this.authorizationService.save(authorization); - return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken); + Map additionalParameters = Collections.emptyMap(); + if (idToken != null) { + additionalParameters = new HashMap<>(); + additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue()); + } + + return new OAuth2AccessTokenAuthenticationToken( + registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters); } @Override diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIssuerUtil.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIssuerUtil.java index 07734fa..9ea4e00 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIssuerUtil.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIssuerUtil.java @@ -20,14 +20,17 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken2; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.jose.JoseHeader; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.util.StringUtils; import java.time.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.Collections; import java.util.Set; @@ -43,7 +46,7 @@ class OAuth2TokenIssuerUtil { static Jwt issueJwtAccessToken(JwtEncoder jwtEncoder, String subject, String audience, Set scopes, Duration tokenTimeToLive) { JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build(); - String issuer = "https://oauth2.provider.com"; // TODO Allow configuration for issuer claim + String issuer = "http://auth-server:9000"; // TODO Allow configuration for issuer claim Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(tokenTimeToLive); @@ -60,6 +63,31 @@ class OAuth2TokenIssuerUtil { return jwtEncoder.encode(joseHeader, jwtClaimsSet); } + static Jwt issueIdToken(JwtEncoder jwtEncoder, String subject, String audience, String nonce) { + JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build(); + + String issuer = "http://auth-server:9000"; // TODO Allow configuration for issuer claim + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES); // TODO Allow configuration for id token time-to-live + + JwtClaimsSet.Builder builder = JwtClaimsSet.builder() + .issuer(issuer) + .subject(subject) + .audience(Collections.singletonList(audience)) + .issuedAt(issuedAt) + .expiresAt(expiresAt) + .claim(IdTokenClaimNames.AZP, audience); + if (StringUtils.hasText(nonce)) { + builder.claim(IdTokenClaimNames.NONCE, nonce); + } + + // TODO Add 'auth_time' claim + + JwtClaimsSet jwtClaimsSet = builder.build(); + + return jwtEncoder.encode(joseHeader, jwtClaimsSet); + } + static OAuth2RefreshToken issueRefreshToken(Duration tokenTimeToLive) { Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(tokenTimeToLive); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java index c3e634b..27a31f2 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java @@ -30,6 +30,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; @@ -40,7 +41,10 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Auth import org.springframework.security.oauth2.server.authorization.token.OAuth2Tokens; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.NegatedRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -120,10 +124,24 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter { Assert.hasText(authorizationEndpointUri, "authorizationEndpointUri cannot be empty"); this.registeredClientRepository = registeredClientRepository; this.authorizationService = authorizationService; - this.authorizationRequestMatcher = new AntPathRequestMatcher( + + RequestMatcher authorizationRequestGetMatcher = new AntPathRequestMatcher( authorizationEndpointUri, HttpMethod.GET.name()); - this.userConsentMatcher = new AntPathRequestMatcher( + RequestMatcher authorizationRequestPostMatcher = new AntPathRequestMatcher( authorizationEndpointUri, HttpMethod.POST.name()); + RequestMatcher openidScopeMatcher = request -> { + String scope = request.getParameter(OAuth2ParameterNames.SCOPE); + return StringUtils.hasText(scope) && scope.contains(OidcScopes.OPENID); + }; + RequestMatcher consentActionMatcher = request -> + request.getParameter(UserConsentPage.CONSENT_ACTION_PARAMETER_NAME) != null; + this.authorizationRequestMatcher = new OrRequestMatcher( + authorizationRequestGetMatcher, + new AndRequestMatcher( + authorizationRequestPostMatcher, openidScopeMatcher, + new NegatedRequestMatcher(consentActionMatcher))); + this.userConsentMatcher = new AndRequestMatcher( + authorizationRequestPostMatcher, consentActionMatcher); } @Override @@ -289,7 +307,8 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter { createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI)); return; } - } else if (registeredClient.getRedirectUris().size() != 1) { + } else if (authorizationRequestContext.isAuthenticationRequest() || // redirect_uri is REQUIRED for OpenID Connect + registeredClient.getRedirectUris().size() != 1) { authorizationRequestContext.setError( createError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI)); return; @@ -476,6 +495,10 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter { return this.redirectUri; } + private boolean isAuthenticationRequest() { + return getScopes().contains(OidcScopes.OPENID); + } + protected String resolveRedirectUri() { return StringUtils.hasText(getRedirectUri()) ? getRedirectUri() : diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java index f2eb013..1fe0d5a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java @@ -44,6 +44,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; @@ -161,7 +162,7 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter { OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication); - sendAccessTokenResponse(response, accessTokenAuthentication.getAccessToken(), accessTokenAuthentication.getRefreshToken()); + sendAccessTokenResponse(response, accessTokenAuthentication); } catch (OAuth2AuthenticationException ex) { SecurityContextHolder.clearContext(); @@ -169,8 +170,12 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter { } } - private void sendAccessTokenResponse(HttpServletResponse response, OAuth2AccessToken accessToken, - OAuth2RefreshToken refreshToken) throws IOException { + private void sendAccessTokenResponse(HttpServletResponse response, + OAuth2AccessTokenAuthenticationToken accessTokenAuthentication) throws IOException { + + OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken(); + OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken(); + Map additionalParameters = accessTokenAuthentication.getAdditionalParameters(); OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue()) @@ -182,6 +187,9 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter { if (refreshToken != null) { builder.refreshToken(refreshToken.getTokenValue()); } + if (!CollectionUtils.isEmpty(additionalParameters)) { + builder.additionalParameters(additionalParameters); + } OAuth2AccessTokenResponse accessTokenResponse = builder.build(); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilter.java index ee699c8..d087031 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilter.java @@ -23,6 +23,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp import org.springframework.security.oauth2.core.oidc.OidcProviderConfiguration; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.core.oidc.http.converter.OidcProviderConfigurationHttpMessageConverter; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -86,6 +87,7 @@ public class OidcProviderConfigurationEndpointFilter extends OncePerRequestFilte .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) .grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue()) .subjectType("public") + .idTokenSigningAlgorithm(SignatureAlgorithm.RS256.getName()) .scope(OidcScopes.OPENID) .build(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java index 12a8791..874031b 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java @@ -15,25 +15,59 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization; +import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.crypto.key.CryptoKeySource; +import org.springframework.security.crypto.key.StaticKeyGeneratingCryptoKeySource; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.TokenType; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; +import org.springframework.security.oauth2.server.authorization.token.OAuth2AuthorizationCode; +import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter; +import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter; import org.springframework.security.oauth2.server.authorization.web.OidcProviderConfigurationEndpointFilter; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -44,6 +78,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ public class OidcTests { private static final String issuerUrl = "https://example.com/issuer1"; + private static RegisteredClientRepository registeredClientRepository; + private static OAuth2AuthorizationService authorizationService; + private static CryptoKeySource keySource; @Rule public final SpringTestRule spring = new SpringTestRule(); @@ -51,14 +88,26 @@ public class OidcTests { @Autowired private MockMvc mvc; + @BeforeClass + public static void init() { + registeredClientRepository = mock(RegisteredClientRepository.class); + authorizationService = mock(OAuth2AuthorizationService.class); + keySource = new StaticKeyGeneratingCryptoKeySource(); + } + + @Before + public void setup() { + reset(registeredClientRepository); + reset(authorizationService); + } + @Test public void requestWhenConfigurationRequestAndIssuerSetThenReturnConfigurationResponse() throws Exception { this.spring.register(AuthorizationServerConfigurationWithIssuer.class).autowire(); this.mvc.perform(get(OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)) .andExpect(status().is2xxSuccessful()) - .andExpect(jsonPath("issuer").value(issuerUrl)) - .andReturn(); + .andExpect(jsonPath("issuer").value(issuerUrl)); } @Test @@ -85,18 +134,98 @@ public class OidcTests { ); } + @Test + public void requestWhenAuthenticationRequestThenTokenResponseIncludesIdToken() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build(); + when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + MvcResult mvcResult = this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI) + .params(getAuthorizationRequestParameters(registeredClient)) + .with(user("user"))) + .andExpect(status().is3xxRedirection()) + .andReturn(); + assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://example.com\\?code=.{15,}&state=state"); + + verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId())); + + ArgumentCaptor authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); + verify(authorizationService).save(authorizationCaptor.capture()); + OAuth2Authorization authorization = authorizationCaptor.getValue(); + + when(authorizationService.findByToken( + eq(authorization.getTokens().getToken(OAuth2AuthorizationCode.class).getTokenValue()), + eq(TokenType.AUTHORIZATION_CODE))) + .thenReturn(authorization); + + this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI) + .params(getTokenRequestParameters(registeredClient, authorization)) + .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( + registeredClient.getClientId(), registeredClient.getClientSecret()))) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store"))) + .andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache"))) + .andExpect(jsonPath("$.access_token").isNotEmpty()) + .andExpect(jsonPath("$.token_type").isNotEmpty()) + .andExpect(jsonPath("$.expires_in").isNotEmpty()) + .andExpect(jsonPath("$.refresh_token").isNotEmpty()) + .andExpect(jsonPath("$.scope").isNotEmpty()) + .andExpect(jsonPath("$.id_token").isNotEmpty()); + + verify(registeredClientRepository, times(2)).findByClientId(eq(registeredClient.getClientId())); + verify(authorizationService).findByToken( + eq(authorization.getTokens().getToken(OAuth2AuthorizationCode.class).getTokenValue()), + eq(TokenType.AUTHORIZATION_CODE)); + verify(authorizationService, times(2)).save(any()); + } + + private static MultiValueMap getAuthorizationRequestParameters(RegisteredClient registeredClient) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue()); + parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()); + parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next()); + parameters.set(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " ")); + parameters.set(OAuth2ParameterNames.STATE, "state"); + return parameters; + } + + private static MultiValueMap getTokenRequestParameters(RegisteredClient registeredClient, + OAuth2Authorization authorization) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); + parameters.set(OAuth2ParameterNames.CODE, authorization.getTokens().getToken(OAuth2AuthorizationCode.class).getTokenValue()); + parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next()); + return parameters; + } + + private static String encodeBasicAuth(String clientId, String secret) throws Exception { + clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name()); + secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name()); + String credentialsString = clientId + ":" + secret; + byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8)); + return new String(encodedBytes, StandardCharsets.UTF_8); + } + @EnableWebSecurity @Import(OAuth2AuthorizationServerConfiguration.class) static class AuthorizationServerConfiguration { @Bean RegisteredClientRepository registeredClientRepository() { - return mock(RegisteredClientRepository.class); + return registeredClientRepository; + } + + @Bean + OAuth2AuthorizationService authorizationService() { + return authorizationService; } @Bean CryptoKeySource keySource() { - return mock(CryptoKeySource.class); + return keySource; } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfigurationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfigurationTests.java index 8ea9f28..791d310 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfigurationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfigurationTests.java @@ -41,7 +41,8 @@ public class OidcProviderConfigurationTests { .jwkSetUri("https://example.com/issuer1/oauth2/jwks") .scope("openid") .responseType("code") - .subjectType("public"); + .subjectType("public") + .idTokenSigningAlgorithm("RS256"); @Test public void buildWhenAllRequiredClaimsAndAdditionalClaimsThenCreated() { @@ -55,6 +56,7 @@ public class OidcProviderConfigurationTests { .grantType("authorization_code") .grantType("client_credentials") .subjectType("public") + .idTokenSigningAlgorithm("RS256") .tokenEndpointAuthenticationMethod("client_secret_basic") .claim("a-claim", "a-value") .build(); @@ -67,6 +69,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); assertThat(providerConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials"); assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); + assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic"); assertThat(providerConfiguration.getClaim("a-claim")).isEqualTo("a-value"); } @@ -81,6 +84,7 @@ public class OidcProviderConfigurationTests { .scope("openid") .responseType("code") .subjectType("public") + .idTokenSigningAlgorithm("RS256") .build(); assertThat(providerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1")); @@ -91,6 +95,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); assertThat(providerConfiguration.getGrantTypes()).isNull(); assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); + assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); } @@ -104,6 +109,7 @@ public class OidcProviderConfigurationTests { claims.put(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid")); claims.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code")); claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.singletonList("public")); + claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256")); claims.put("some-claim", "some-value"); OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build(); @@ -116,6 +122,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); assertThat(providerConfiguration.getGrantTypes()).isNull(); assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); + assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); assertThat(providerConfiguration.getClaim("some-claim")).isEqualTo("some-value"); } @@ -130,6 +137,7 @@ public class OidcProviderConfigurationTests { claims.put(OidcProviderMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid")); claims.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code")); claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.singletonList("public")); + claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256")); claims.put("some-claim", "some-value"); OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build(); @@ -142,6 +150,7 @@ public class OidcProviderConfigurationTests { assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); assertThat(providerConfiguration.getGrantTypes()).isNull(); assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); + assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); assertThat(providerConfiguration.getClaim("some-claim")).isEqualTo("some-value"); } @@ -332,6 +341,42 @@ public class OidcProviderConfigurationTests { .hasMessageContaining("subjectTypes cannot be empty"); } + @Test + public void buildWhenMissingIdTokenSigningAlgorithmsThenThrowIllegalArgumentException() { + OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder + .claims((claims) -> claims.remove(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED)); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("idTokenSigningAlgorithms cannot be null"); + } + + @Test + public void buildWhenIdTokenSigningAlgorithmsNotListThenThrowIllegalArgumentException() { + OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder + .claims((claims) -> { + claims.remove(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED); + claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, "RS256"); + }); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("idTokenSigningAlgorithms must be of type List"); + } + + @Test + public void buildWhenIdTokenSigningAlgorithmsEmptyListThenThrowIllegalArgumentException() { + OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder + .claims((claims) -> { + claims.remove(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED); + claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.emptyList()); + }); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("idTokenSigningAlgorithms cannot be empty"); + } + @Test public void responseTypesWhenAddingOrRemovingThenCorrectValues() { OidcProviderConfiguration configuration = this.minimalConfigurationBuilder @@ -368,6 +413,19 @@ public class OidcProviderConfigurationTests { assertThat(configuration.getSubjectTypes()).containsExactly("some-subject-type"); } + @Test + public void idTokenSigningAlgorithmsWhenAddingOrRemovingThenCorrectValues() { + OidcProviderConfiguration configuration = this.minimalConfigurationBuilder + .idTokenSigningAlgorithm("should-be-removed") + .idTokenSigningAlgorithms(signingAlgorithms -> { + signingAlgorithms.clear(); + signingAlgorithms.add("ES256"); + }) + .build(); + + assertThat(configuration.getIdTokenSigningAlgorithms()).containsExactly("ES256"); + } + @Test public void scopesWhenAddingOrRemovingThenCorrectValues() { OidcProviderConfiguration configuration = this.minimalConfigurationBuilder diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java index 37b4284..2d4f82e 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java @@ -65,7 +65,8 @@ public class OidcProviderConfigurationHttpMessageConverterTests { + " \"token_endpoint\": \"https://example.com/issuer1/oauth2/token\",\n" + " \"jwks_uri\": \"https://example.com/issuer1/oauth2/jwks\",\n" + " \"response_types_supported\": [\"code\"],\n" - + " \"subject_types_supported\": [\"public\"]\n" + + " \"subject_types_supported\": [\"public\"],\n" + + " \"id_token_signing_alg_values_supported\": [\"RS256\"]\n" + "}\n"; // @formatter:on MockClientHttpResponse response = new MockClientHttpResponse(providerConfigurationResponse.getBytes(), HttpStatus.OK); @@ -78,6 +79,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests { assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks")); assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); + assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getScopes()).isNull(); assertThat(providerConfiguration.getGrantTypes()).isNull(); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); @@ -95,6 +97,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests { + " \"response_types_supported\": [\"code\"],\n" + " \"grant_types_supported\": [\"authorization_code\", \"client_credentials\"],\n" + " \"subject_types_supported\": [\"public\"],\n" + + " \"id_token_signing_alg_values_supported\": [\"RS256\"],\n" + " \"token_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n" + " \"custom_claim\": \"value\",\n" + " \"custom_collection_claim\": [\"value1\", \"value2\"]\n" @@ -112,6 +115,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests { assertThat(providerConfiguration.getResponseTypes()).containsExactly("code"); assertThat(providerConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials"); assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); + assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic"); assertThat(providerConfiguration.getClaim("custom_claim")).isEqualTo("value"); assertThat(providerConfiguration.getClaimAsStringList("custom_collection_claim")).containsExactlyInAnyOrder("value1", "value2"); @@ -155,6 +159,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests { .grantType("authorization_code") .grantType("client_credentials") .subjectType("public") + .idTokenSigningAlgorithm("RS256") .tokenEndpointAuthenticationMethod("client_secret_basic") .claim("custom_claim", "value") .claim("custom_collection_claim", Arrays.asList("value1", "value2")) @@ -172,6 +177,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests { assertThat(providerConfigurationResponse).contains("\"response_types_supported\":[\"code\"]"); assertThat(providerConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\"]"); assertThat(providerConfigurationResponse).contains("\"subject_types_supported\":[\"public\"]"); + assertThat(providerConfigurationResponse).contains("\"id_token_signing_alg_values_supported\":[\"RS256\"]"); assertThat(providerConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\"]"); assertThat(providerConfigurationResponse).contains("\"custom_claim\":\"value\""); assertThat(providerConfigurationResponse).contains("\"custom_collection_claim\":[\"value1\",\"value2\"]"); @@ -194,6 +200,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests { .jwkSetUri("https://example.com/issuer1/oauth2/jwks") .responseType("code") .subjectType("public") + .idTokenSigningAlgorithm("RS256") .build(); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationTokenTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationTokenTests.java index ca237e7..58c5783 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationTokenTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AccessTokenAuthenticationTokenTests.java @@ -17,10 +17,15 @@ package org.springframework.security.oauth2.server.authorization.authentication; import org.junit.Test; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken2; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -36,6 +41,9 @@ public class OAuth2AccessTokenAuthenticationTokenTests { new OAuth2ClientAuthenticationToken(this.registeredClient); private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plusSeconds(300)); + private OAuth2RefreshToken refreshToken = new OAuth2RefreshToken2( + "refresh-token", Instant.now(), Instant.now().plus(1, ChronoUnit.DAYS)); + private Map additionalParameters = Collections.singletonMap("custom-param", "custom-value"); @Test public void constructorWhenRegisteredClientNullThenThrowIllegalArgumentException() { @@ -58,13 +66,23 @@ public class OAuth2AccessTokenAuthenticationTokenTests { .hasMessage("accessToken cannot be null"); } + @Test + public void constructorWhenAdditionalParametersNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AccessTokenAuthenticationToken( + this.registeredClient, this.clientPrincipal, this.accessToken, this.refreshToken, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("additionalParameters cannot be null"); + } + @Test public void constructorWhenAllValuesProvidedThenCreated() { OAuth2AccessTokenAuthenticationToken authentication = new OAuth2AccessTokenAuthenticationToken( - this.registeredClient, this.clientPrincipal, this.accessToken); + this.registeredClient, this.clientPrincipal, this.accessToken, this.refreshToken, this.additionalParameters); assertThat(authentication.getPrincipal()).isEqualTo(this.clientPrincipal); assertThat(authentication.getCredentials().toString()).isEmpty(); assertThat(authentication.getRegisteredClient()).isEqualTo(this.registeredClient); assertThat(authentication.getAccessToken()).isEqualTo(this.accessToken); + assertThat(authentication.getRefreshToken()).isEqualTo(this.refreshToken); + assertThat(authentication.getAdditionalParameters()).isEqualTo(this.additionalParameters); } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java index 0439a0c..4aa4a2d 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java @@ -25,6 +25,9 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.oauth2.jose.JoseHeaderNames; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; @@ -49,9 +52,11 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -251,6 +256,43 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { assertThat(updatedAuthorization.getTokens().getTokenMetadata(authorizationCode).isInvalidated()).isTrue(); } + @Test + public void authenticateWhenValidCodeAndAuthenticationRequestThenReturnIdToken() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build(); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); + when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) + .thenReturn(authorization); + + OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); + OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( + OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST); + OAuth2AuthorizationCodeAuthenticationToken authentication = + new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, authorizationRequest.getRedirectUri(), null); + + when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt()); + + OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = + (OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication); + + verify(this.jwtEncoder, times(2)).encode(any(), any()); // Access token and ID Token + + ArgumentCaptor authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); + verify(this.authorizationService).save(authorizationCaptor.capture()); + OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue(); + + assertThat(accessTokenAuthentication.getRegisteredClient().getId()).isEqualTo(updatedAuthorization.getRegisteredClientId()); + assertThat(accessTokenAuthentication.getPrincipal()).isEqualTo(clientPrincipal); + assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getTokens().getAccessToken()); + assertThat(accessTokenAuthentication.getRefreshToken()).isNotNull(); + assertThat(accessTokenAuthentication.getRefreshToken()).isEqualTo(updatedAuthorization.getTokens().getRefreshToken()); + OAuth2AuthorizationCode authorizationCode = updatedAuthorization.getTokens().getToken(OAuth2AuthorizationCode.class); + assertThat(updatedAuthorization.getTokens().getTokenMetadata(authorizationCode).isInvalidated()).isTrue(); + OidcIdToken idToken = updatedAuthorization.getTokens().getToken(OidcIdToken.class); + assertThat(idToken).isNotNull(); + assertThat(accessTokenAuthentication.getAdditionalParameters()) + .containsExactly(entry(OidcParameterNames.ID_TOKEN, idToken.getTokenValue())); + } + @Test public void authenticateWhenTokenTimeToLiveConfiguredThenTokenExpirySet() { Duration accessTokenTTL = Duration.ofHours(2); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java index b78fb43..0afb357 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java @@ -148,7 +148,7 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests { public void authenticateWhenScopeRequestedThenAccessTokenContainsScope() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build(); OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); - Set requestedScope = Collections.singleton("openid"); + Set requestedScope = Collections.singleton("scope1"); OAuth2ClientCredentialsAuthenticationToken authentication = new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal, requestedScope); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java index b63c1f7..93ae41f 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java @@ -31,9 +31,7 @@ public class TestRegisteredClients { .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) .redirectUri("https://example.com") - .scope("openid") - .scope("profile") - .scope("email"); + .scope("scope1"); } public static RegisteredClient.Builder registeredClient2() { @@ -46,9 +44,6 @@ public class TestRegisteredClients { .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) .clientAuthenticationMethod(ClientAuthenticationMethod.POST) .redirectUri("https://example.com") - .scope("openid") - .scope("profile") - .scope("email") .scope("scope1") .scope("scope2"); } @@ -59,9 +54,7 @@ public class TestRegisteredClients { .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) .redirectUri("https://example.com") - .scope("openid") - .scope("profile") - .scope("email") + .scope("scope1") .clientSettings(clientSettings -> clientSettings.requireProofKey(true)); } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java index 86d9afc..61b5dd0 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java @@ -32,6 +32,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; @@ -168,6 +169,20 @@ public class OAuth2AuthorizationEndpointFilterTests { OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); } + @Test + public void doFilterWhenAuthenticationRequestMissingRedirectUriThenInvalidRequestError() throws Exception { + // redirect_uri is REQUIRED for OpenID Connect requests + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build(); + when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId())))) + .thenReturn(registeredClient); + + doFilterWhenAuthorizationRequestInvalidParameterThenError( + registeredClient, + OAuth2ParameterNames.REDIRECT_URI, + OAuth2ErrorCodes.INVALID_REQUEST, + request -> request.removeParameter(OAuth2ParameterNames.REDIRECT_URI)); + } + @Test public void doFilterWhenAuthorizationRequestInvalidRedirectUriThenInvalidRequestError() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); @@ -394,7 +409,7 @@ public class OAuth2AuthorizationEndpointFilterTests { } @Test - public void doFilterWhenAuthorizationRequestValidNotAuthenticatedThenContinueChainToCommenceAuthentication() throws Exception { + public void doFilterWhenAuthorizationRequestNotAuthenticatedThenContinueChainToCommenceAuthentication() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId())))) .thenReturn(registeredClient); @@ -411,12 +426,27 @@ public class OAuth2AuthorizationEndpointFilterTests { } @Test - public void doFilterWhenAuthorizationRequestValidThenAuthorizationResponse() throws Exception { + public void doFilterWhenAuthorizationRequestGetThenAuthorizationResponse() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + MockHttpServletRequest request = createAuthorizationRequest(registeredClient); + doFilterWhenAuthorizationRequestThenAuthorizationResponse(registeredClient, request); + } + + @Test + public void doFilterWhenAuthorizationRequestPostThenAuthorizationResponse() throws Exception { + // OpenID Connect requests support POST method + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build(); + MockHttpServletRequest request = createAuthorizationRequest(registeredClient); + request.setMethod("POST"); + doFilterWhenAuthorizationRequestThenAuthorizationResponse(registeredClient, request); + } + + private void doFilterWhenAuthorizationRequestThenAuthorizationResponse( + RegisteredClient registeredClient, MockHttpServletRequest request) throws Exception { + when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId())))) .thenReturn(registeredClient); - MockHttpServletRequest request = createAuthorizationRequest(registeredClient); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); @@ -455,7 +485,7 @@ public class OAuth2AuthorizationEndpointFilterTests { } @Test - public void doFilterWhenPkceRequiredAndAuthorizationRequestValidThenAuthorizationResponse() throws Exception { + public void doFilterWhenPkceRequiredAndAuthorizationRequestThenAuthorizationResponse() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient() .clientSettings(clientSettings -> clientSettings.requireProofKey(true)) .build(); @@ -501,7 +531,7 @@ public class OAuth2AuthorizationEndpointFilterTests { } @Test - public void doFilterWhenUserConsentRequiredAndAuthorizationRequestValidThenUserConsentResponse() throws Exception { + public void doFilterWhenUserConsentRequiredAndAuthorizationRequestThenUserConsentResponse() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient() .clientSettings(clientSettings -> clientSettings.requireUserConsent(true)) .build(); @@ -722,7 +752,7 @@ public class OAuth2AuthorizationEndpointFilterTests { OAuth2ParameterNames.CLIENT_ID, OAuth2ErrorCodes.ACCESS_DENIED, DEFAULT_ERROR_URI, - request -> request.removeParameter("consent_action")); + request -> request.setParameter("consent_action", "cancel")); verify(this.authorizationService).remove(eq(authorization)); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java index 9235c0a..223c8c7 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java @@ -33,6 +33,7 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken2; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; @@ -53,10 +54,13 @@ import javax.servlet.http.HttpServletResponse; import java.time.Duration; import java.time.Instant; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -199,16 +203,19 @@ public class OAuth2TokenEndpointFilterTests { } @Test - public void doFilterWhenAuthorizationCodeTokenRequestValidThenAccessTokenResponse() throws Exception { + public void doFilterWhenAuthorizationCodeTokenRequestThenAccessTokenResponse() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); OAuth2AccessToken accessToken = new OAuth2AccessToken( OAuth2AccessToken.TokenType.BEARER, "token", Instant.now(), Instant.now().plus(Duration.ofHours(1)), new HashSet<>(Arrays.asList("scope1", "scope2"))); + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken2( + "refresh-token", Instant.now(), Instant.now().plus(Duration.ofDays(1))); + Map additionalParameters = Collections.singletonMap("custom-param", "custom-value"); OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = new OAuth2AccessTokenAuthenticationToken( - registeredClient, clientPrincipal, accessToken); + registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters); when(this.authenticationManager.authenticate(any())).thenReturn(accessTokenAuthentication); @@ -247,6 +254,8 @@ public class OAuth2TokenEndpointFilterTests { assertThat(accessTokenResult.getExpiresAt()).isBetween( accessToken.getExpiresAt().minusSeconds(1), accessToken.getExpiresAt().plusSeconds(1)); assertThat(accessTokenResult.getScopes()).isEqualTo(accessToken.getScopes()); + assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo(refreshToken.getTokenValue()); + assertThat(accessTokenResponse.getAdditionalParameters()).containsExactly(entry("custom-param", "custom-value")); } @Test @@ -260,7 +269,7 @@ public class OAuth2TokenEndpointFilterTests { } @Test - public void doFilterWhenClientCredentialsTokenRequestValidThenAccessTokenResponse() throws Exception { + public void doFilterWhenClientCredentialsTokenRequestThenAccessTokenResponse() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build(); Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); OAuth2AccessToken accessToken = new OAuth2AccessToken( @@ -338,7 +347,7 @@ public class OAuth2TokenEndpointFilterTests { } @Test - public void doFilterWhenRefreshTokenRequestValidThenAccessTokenResponse() throws Exception { + public void doFilterWhenRefreshTokenRequestThenAccessTokenResponse() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient); OAuth2AccessToken accessToken = new OAuth2AccessToken( diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilterTests.java index 8bd3c2e..2d89305 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OidcProviderConfigurationEndpointFilterTests.java @@ -112,6 +112,7 @@ public class OidcProviderConfigurationEndpointFilterTests { assertThat(providerConfigurationResponse).contains("\"response_types_supported\":[\"code\"]"); assertThat(providerConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\"]"); assertThat(providerConfigurationResponse).contains("\"subject_types_supported\":[\"public\"]"); + assertThat(providerConfigurationResponse).contains("\"id_token_signing_alg_values_supported\":[\"RS256\"]"); assertThat(providerConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]"); }