From da9f027fa4dbc32d78029032f46fe952366096ad Mon Sep 17 00:00:00 2001 From: Mark Heckler Date: Sun, 1 Sep 2019 18:47:13 -0500 Subject: [PATCH] Add nonce to OIDC Authentication Request Fixes gh-4442 --- .../client/OAuth2ClientConfigurerTests.java | 10 +- ...thorizationCodeAuthenticationProvider.java | 30 ++++- ...tionCodeReactiveAuthenticationManager.java | 39 +++++- .../OidcIdTokenDecoderFactory.java | 3 + .../authentication/OidcIdTokenValidator.java | 7 - .../ReactiveOidcIdTokenDecoderFactory.java | 3 + ...ultOAuth2AuthorizationRequestResolver.java | 41 +++++- ...verOAuth2AuthorizationRequestResolver.java | 47 +++++-- ...zationCodeAuthenticationProviderTests.java | 120 +++++++++++++++--- ...uth2AuthorizationRequestResolverTests.java | 35 +++-- ...thorizationRequestRedirectFilterTests.java | 13 +- ...uth2AuthorizationRequestResolverTests.java | 8 +- .../oidc/endpoint/OidcParameterNames.java | 8 +- 13 files changed, 295 insertions(+), 69 deletions(-) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java index 86e4b4e3c4..4605bf821d 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java @@ -75,6 +75,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * Tests for {@link OAuth2ClientConfigurer}. * * @author Joe Grandja + * @author Mark Heckler */ public class OAuth2ClientConfigurerTests { private static ClientRegistrationRepository clientRegistrationRepository; @@ -138,7 +139,8 @@ public class OAuth2ClientConfigurerTests { assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?" + "response_type=code&client_id=client-1&" + "scope=user&state=.{15,}&" + - "redirect_uri=http://localhost/client-1"); + "redirect_uri=http://localhost/client-1&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } @Test @@ -151,7 +153,8 @@ public class OAuth2ClientConfigurerTests { assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?" + "response_type=code&client_id=client-1&" + "scope=user&state=.{15,}&" + - "redirect_uri=http://localhost/client-1"); + "redirect_uri=http://localhost/client-1&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } @Test @@ -203,7 +206,8 @@ public class OAuth2ClientConfigurerTests { assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?" + "response_type=code&client_id=client-1&" + "scope=user&state=.{15,}&" + - "redirect_uri=http://localhost/client-1"); + "redirect_uri=http://localhost/client-1&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); verify(requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class)); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java index 73227b6989..d61f270ec7 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java @@ -43,6 +43,10 @@ import org.springframework.security.oauth2.jwt.JwtDecoderFactory; import org.springframework.security.oauth2.jwt.JwtException; import org.springframework.util.Assert; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; import java.util.Collection; import java.util.Map; @@ -61,6 +65,7 @@ import java.util.Map; * to complete the authentication. * * @author Joe Grandja + * @author Mark Heckler * @since 5.0 * @see OAuth2LoginAuthenticationToken * @see OAuth2AccessTokenResponseClient @@ -75,6 +80,7 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter"; private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter"; private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token"; + private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce"; private final OAuth2AccessTokenResponseClient accessTokenResponseClient; private final OAuth2UserService userService; private JwtDecoderFactory jwtDecoderFactory = new OidcIdTokenDecoderFactory(); @@ -152,7 +158,23 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati null); throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); } - OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse); + OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse); + + String requestNonce = authorizationRequest.getAttribute(OidcParameterNames.NONCE); + if (requestNonce != null) { + String nonceHash; + + try { + nonceHash = createHash(requestNonce); + } catch (NoSuchAlgorithmException e) { + throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_NONCE_ERROR_CODE)); + } + + String nonceHashClaim = idToken.getClaim(OidcParameterNames.NONCE); + if (nonceHashClaim == null || !nonceHashClaim.equals(nonceHash)) { + throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_NONCE_ERROR_CODE)); + } + } OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest( clientRegistration, accessTokenResponse.getAccessToken(), idToken, additionalParameters)); @@ -211,4 +233,10 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()); return idToken; } + + private String createHash(String nonce) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(nonce.getBytes(StandardCharsets.US_ASCII)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java index 8a215efdf8..5e2c1b8150 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java @@ -43,13 +43,17 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory; import org.springframework.util.Assert; import reactor.core.publisher.Mono; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; import java.util.Collection; import java.util.Map; /** * An implementation of an {@link org.springframework.security.authentication.AuthenticationProvider} for OAuth 2.0 Login, * which leverages the OAuth 2.0 Authorization Code Grant Flow. - * + *

* This {@link org.springframework.security.authentication.AuthenticationProvider} is responsible for authenticating * an Authorization Code credential with the Authorization Server's Token Endpoint * and if valid, exchanging it for an Access Token credential. @@ -77,6 +81,7 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter"; private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter"; private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token"; + private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce"; private final ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; @@ -170,7 +175,8 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements } return createOidcToken(clientRegistration, accessTokenResponse) - .map(idToken -> new OidcUserRequest(clientRegistration, accessToken, idToken, additionalParameters)) + .doOnNext(idToken -> validateNonce(authorizationCodeAuthentication, idToken)) + .map(idToken -> new OidcUserRequest(clientRegistration, accessToken, idToken, additionalParameters)) .flatMap(this.userService::loadUser) .map(oauth2User -> { Collection mappedAuthorities = @@ -192,4 +198,33 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements return jwtDecoder.decode(rawIdToken) .map(jwt -> new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims())); } + + private Mono validateNonce(OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication, OidcIdToken idToken) { + String requestNonce = authorizationCodeAuthentication + .getAuthorizationExchange() + .getAuthorizationRequest() + .getAttribute(OidcParameterNames.NONCE); + if (requestNonce != null) { + String nonceHash; + + try { + nonceHash = createHash(requestNonce); + } catch (NoSuchAlgorithmException e) { + throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_NONCE_ERROR_CODE)); + } + + String nonceHashClaim = idToken.getClaim(OidcParameterNames.NONCE); + if (nonceHashClaim == null || !nonceHashClaim.equals(nonceHash)) { + throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_NONCE_ERROR_CODE)); + } + } + + return Mono.just(idToken); + } + + private String createHash(String nonce) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(nonce.getBytes(StandardCharsets.US_ASCII)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java index 2ef5d81142..da51f71d62 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java @@ -57,6 +57,7 @@ import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withSecre * * @author Joe Grandja * @author Rafael Dominguez + * @author Mark Heckler * @since 5.2 * @see JwtDecoderFactory * @see ClientRegistration @@ -88,12 +89,14 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class)); Converter instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class)); Converter urlConverter = getConverter(TypeDescriptor.valueOf(URL.class)); + Converter stringConverter = getConverter(TypeDescriptor.valueOf(String.class)); Converter collectionStringConverter = getConverter( TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class))); Map> claimTypeConverters = new HashMap<>(); claimTypeConverters.put(IdTokenClaimNames.ISS, urlConverter); claimTypeConverters.put(IdTokenClaimNames.AUD, collectionStringConverter); + claimTypeConverters.put(IdTokenClaimNames.NONCE, stringConverter); claimTypeConverters.put(IdTokenClaimNames.EXP, instantConverter); claimTypeConverters.put(IdTokenClaimNames.IAT, instantConverter); claimTypeConverters.put(IdTokenClaimNames.AUTH_TIME, instantConverter); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidator.java index 806c837112..9a23f25c14 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidator.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidator.java @@ -107,13 +107,6 @@ public final class OidcIdTokenValidator implements OAuth2TokenValidator { invalidClaims.put(IdTokenClaimNames.IAT, idToken.getIssuedAt()); } - // 11. If a nonce value was sent in the Authentication Request, - // a nonce Claim MUST be present and its value checked to verify - // that it is the same value as the one that was sent in the Authentication Request. - // The Client SHOULD check the nonce value for replay attacks. - // The precise method for detecting replay attacks is Client specific. - // TODO Depends on gh-4442 - if (!invalidClaims.isEmpty()) { return OAuth2TokenValidatorResult.failure(invalidIdToken(invalidClaims)); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java index 16dfa5461c..6661efb7ed 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java @@ -57,6 +57,7 @@ import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.w * * @author Joe Grandja * @author Rafael Dominguez + * @author Mark Heckler * @since 5.2 * @see ReactiveJwtDecoderFactory * @see ClientRegistration @@ -88,12 +89,14 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod Converter booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class)); Converter instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class)); Converter urlConverter = getConverter(TypeDescriptor.valueOf(URL.class)); + Converter stringConverter = getConverter(TypeDescriptor.valueOf(String.class)); Converter collectionStringConverter = getConverter( TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class))); Map> claimTypeConverters = new HashMap<>(); claimTypeConverters.put(IdTokenClaimNames.ISS, urlConverter); claimTypeConverters.put(IdTokenClaimNames.AUD, collectionStringConverter); + claimTypeConverters.put(IdTokenClaimNames.NONCE, stringConverter); claimTypeConverters.put(IdTokenClaimNames.EXP, instantConverter); claimTypeConverters.put(IdTokenClaimNames.IAT, instantConverter); claimTypeConverters.put(IdTokenClaimNames.AUTH_TIME, instantConverter); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java index 84e2ec47be..533d29c374 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java @@ -24,6 +24,7 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.web.util.UrlUtils; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Assert; @@ -51,6 +52,7 @@ import java.util.Map; * @author Joe Grandja * @author Rob Winch * @author EddĂș MelĂ©ndez + * @author Mark Heckler * @since 5.1 * @see OAuth2AuthorizationRequestResolver * @see OAuth2AuthorizationRequestRedirectFilter @@ -61,7 +63,7 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au private final ClientRegistrationRepository clientRegistrationRepository; private final AntPathRequestMatcher authorizationRequestMatcher; private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); - private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); + private final StringKeyGenerator stringKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); /** * Constructs a {@code DefaultOAuth2AuthorizationRequestResolver} using the provided parameters. @@ -118,11 +120,15 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au OAuth2AuthorizationRequest.Builder builder; if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { builder = OAuth2AuthorizationRequest.authorizationCode(); + Map additionalParameters = new HashMap<>(); + + addNonceParameters(attributes, additionalParameters); + if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) { - Map additionalParameters = new HashMap<>(); addPkceParameters(attributes, additionalParameters); - builder.additionalParameters(additionalParameters); } + + builder.additionalParameters(additionalParameters); } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { builder = OAuth2AuthorizationRequest.implicit(); } else { @@ -201,6 +207,27 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au .toUriString(); } + /** + * Creates nonce and its hash for use in OpenID Connect Authentication Requests + * + * @param attributes where {@link OidcParameterNames#NONCE} is stored for the token request + * @param additionalParameters where hash of {@link OidcParameterNames#NONCE} is added to the authentication request + * + * @since 5.2 + * @see 15.5.2. Nonce Implementation Notes + * @see 3.1.3.7. ID Token Validation + */ + private void addNonceParameters(Map attributes, Map additionalParameters) { + try { + String nonce = this.stringKeyGenerator.generateKey(); + attributes.put(OidcParameterNames.NONCE, nonce); + + String nonceHash = createHash(nonce); + additionalParameters.put(OidcParameterNames.NONCE, nonceHash); + } catch (NoSuchAlgorithmException ignored) { + } + } + /** * Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests * @@ -214,10 +241,10 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au * @see 4.2. Client Creates the Code Challenge */ private void addPkceParameters(Map attributes, Map additionalParameters) { - String codeVerifier = this.codeVerifierGenerator.generateKey(); + String codeVerifier = this.stringKeyGenerator.generateKey(); attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier); try { - String codeChallenge = createCodeChallenge(codeVerifier); + String codeChallenge = createHash(codeVerifier); additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge); additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); } catch (NoSuchAlgorithmException e) { @@ -225,9 +252,9 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au } } - private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException { + private String createHash(String value) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII)); + byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII)); return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java index f0f4eb38c1..6561c517bf 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java @@ -27,6 +27,7 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.util.Assert; @@ -52,6 +53,7 @@ import java.util.Map; * used to resolve the {@link ClientRegistration} and create the {@link OAuth2AuthorizationRequest}. * * @author Rob Winch + * @author Mark Heckler * @since 5.1 */ public class DefaultServerOAuth2AuthorizationRequestResolver @@ -75,7 +77,7 @@ public class DefaultServerOAuth2AuthorizationRequestResolver private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); - private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); + private final StringKeyGenerator stringKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); /** * Creates a new instance @@ -132,16 +134,18 @@ public class DefaultServerOAuth2AuthorizationRequestResolver OAuth2AuthorizationRequest.Builder builder; if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { builder = OAuth2AuthorizationRequest.authorizationCode(); + Map additionalParameters = new HashMap<>(); + + addNonceParameters(attributes, additionalParameters); + if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) { - Map additionalParameters = new HashMap<>(); addPkceParameters(attributes, additionalParameters); - builder.additionalParameters(additionalParameters); } - } - else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { + + builder.additionalParameters(additionalParameters); + } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { builder = OAuth2AuthorizationRequest.implicit(); - } - else { + } else { throw new IllegalArgumentException( "Invalid Authorization Grant Type (" + clientRegistration.getAuthorizationGrantType().getValue() + ") for Client Registration with Id: " + clientRegistration.getRegistrationId()); @@ -207,6 +211,27 @@ public class DefaultServerOAuth2AuthorizationRequestResolver .toUriString(); } + /** + * Creates nonce and its hash for use in OpenID Connect Authentication Requests + * + * @param attributes where {@link OidcParameterNames#NONCE} is stored for the token request + * @param additionalParameters where hash of {@link OidcParameterNames#NONCE} is added to the authentication request + * + * @since 5.2 + * @see 15.5.2. Nonce Implementation Notes + * @see 3.1.3.7. ID Token Validation + */ + private void addNonceParameters(Map attributes, Map additionalParameters) { + try { + String nonce = this.stringKeyGenerator.generateKey(); + attributes.put(OidcParameterNames.NONCE, nonce); + + String nonceHash = createHash(nonce); + additionalParameters.put(OidcParameterNames.NONCE, nonceHash); + } catch (NoSuchAlgorithmException ignored) { + } + } + /** * Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests * @@ -220,10 +245,10 @@ public class DefaultServerOAuth2AuthorizationRequestResolver * @see 4.2. Client Creates the Code Challenge */ private void addPkceParameters(Map attributes, Map additionalParameters) { - String codeVerifier = this.codeVerifierGenerator.generateKey(); + String codeVerifier = this.stringKeyGenerator.generateKey(); attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier); try { - String codeChallenge = createCodeChallenge(codeVerifier); + String codeChallenge = createHash(codeVerifier); additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge); additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); } catch (NoSuchAlgorithmException e) { @@ -231,9 +256,9 @@ public class DefaultServerOAuth2AuthorizationRequestResolver } } - private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException { + private String createHash(String value) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII)); + byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII)); return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java index 85169e809e..a5776d4842 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -15,25 +15,17 @@ */ package org.springframework.security.oauth2.client.oidc.authentication; -import java.time.Instant; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; - import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; @@ -54,23 +46,34 @@ import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyCollection; -import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.security.oauth2.client.registration.TestClientRegistrations.clientRegistration; import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests.request; import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses.error; import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses.success; -import static org.springframework.security.oauth2.jwt.TestJwts.jwt; /** * Tests for {@link OidcAuthorizationCodeAuthenticationProvider}. * * @author Joe Grandja + * @author Mark Heckler */ public class OidcAuthorizationCodeAuthenticationProviderTests { private ClientRegistration clientRegistration; @@ -81,6 +84,9 @@ public class OidcAuthorizationCodeAuthenticationProviderTests { private OAuth2AccessTokenResponse accessTokenResponse; private OAuth2UserService userService; private OidcAuthorizationCodeAuthenticationProvider authenticationProvider; + private StringKeyGenerator stringKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); + private String nonce = this.stringKeyGenerator.generateKey(); + private String nonceHash; @Rule public ExpectedException exception = ExpectedException.none(); @@ -88,8 +94,21 @@ public class OidcAuthorizationCodeAuthenticationProviderTests { @Before @SuppressWarnings("unchecked") public void setUp() { + try { + nonceHash = createHash(nonce); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + Map attributes = new HashMap<>(); + Map additionalParameters = new HashMap<>(); + addNonceToRequest(attributes, additionalParameters); + this.clientRegistration = clientRegistration().clientId("client1").build(); - this.authorizationRequest = request().scope("openid", "profile", "email").build(); + this.authorizationRequest = request() + .scope("openid", "profile", "email") + .attributes(attributes) + .additionalParameters(additionalParameters) + .build(); this.authorizationResponse = success().build(); this.authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest, this.authorizationResponse); this.accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class); @@ -228,6 +247,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests { claims.put(IdTokenClaimNames.SUB, "subject1"); claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2")); claims.put(IdTokenClaimNames.AZP, "client1"); + claims.put(IdTokenClaimNames.NONCE, nonceHash); this.setUpIdToken(claims); OidcUser principal = mock(OidcUser.class); @@ -257,6 +277,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests { claims.put(IdTokenClaimNames.SUB, "subject1"); claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2")); claims.put(IdTokenClaimNames.AZP, "client1"); + claims.put(IdTokenClaimNames.NONCE, nonceHash); this.setUpIdToken(claims); OidcUser principal = mock(OidcUser.class); @@ -286,6 +307,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests { claims.put(IdTokenClaimNames.SUB, "subject1"); claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2")); claims.put(IdTokenClaimNames.AZP, "client1"); + claims.put(IdTokenClaimNames.NONCE, nonceHash); this.setUpIdToken(claims); OidcUser principal = mock(OidcUser.class); @@ -302,9 +324,49 @@ public class OidcAuthorizationCodeAuthenticationProviderTests { this.accessTokenResponse.getAdditionalParameters()); } - private void setUpIdToken(Map claims) { - Jwt idToken = jwt().claims(c -> c.putAll(claims)).build(); + // gh-4442 + @Test + public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToUserRequestNoNonce() { + OAuth2AuthorizationRequest authorizationRequestNoNonce = request() + .scope("openid", "profile", "email") + .attributes(new HashMap<>()) + .additionalParameters(new HashMap<>()) + .build(); + OAuth2AuthorizationExchange authorizationExchangeNoNonce = new OAuth2AuthorizationExchange(authorizationRequestNoNonce, this.authorizationResponse); + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://provider.com"); + claims.put(IdTokenClaimNames.SUB, "subject1"); + claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2")); + claims.put(IdTokenClaimNames.AZP, "client1"); + this.setUpIdToken(claims); + + OidcUser principal = mock(OidcUser.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + when(principal.getAuthorities()).thenAnswer( + (Answer>) invocation -> authorities); + ArgumentCaptor userRequestArgCaptor = ArgumentCaptor.forClass(OidcUserRequest.class); + when(this.userService.loadUser(userRequestArgCaptor.capture())).thenReturn(principal); + + this.authenticationProvider.authenticate(new OAuth2LoginAuthenticationToken( + this.clientRegistration, authorizationExchangeNoNonce)); + + assertThat(userRequestArgCaptor.getValue().getAdditionalParameters()).containsAllEntriesOf( + this.accessTokenResponse.getAdditionalParameters()); + } + + private void setUpIdToken(Map claims) { + Jwt idToken = Jwt.withTokenValue("token") + .header("alg", "none") + .audience(Collections.singletonList("https://audience.example.org")) + .expiresAt(Instant.MAX) + .issuedAt(Instant.MIN) + .issuer("https://issuer.example.org") + .jti("jti") + .notBefore(Instant.MIN) + .subject("mock-test-subject") + .claims(c -> c.putAll(claims)) + .build(); JwtDecoder jwtDecoder = mock(JwtDecoder.class); when(jwtDecoder.decode(anyString())).thenReturn(idToken); this.authenticationProvider.setJwtDecoderFactory(registration -> jwtDecoder); @@ -317,6 +379,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests { additionalParameters.put("param1", "value1"); additionalParameters.put("param2", "value2"); additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token"); + additionalParameters.put(IdTokenClaimNames.NONCE, nonceHash); return OAuth2AccessTokenResponse .withToken("access-token-1234") @@ -328,4 +391,25 @@ public class OidcAuthorizationCodeAuthenticationProviderTests { .build(); } + + /** + * Adds nonce for use in OpenID Connect Authentication Requests + * + * @param attributes where {@link IdTokenClaimNames#NONCE} is stored for the token request + * @param additionalParameters where the hash of {@link IdTokenClaimNames#NONCE} is added to be used in the authentication request + * + * @since 5.2 + * @see 15.5.2. Nonce Implementation Notes + * @see 3.1.3.7. ID Token Validation + */ + private void addNonceToRequest(Map attributes, Map additionalParameters) { + attributes.put(IdTokenClaimNames.NONCE, nonce); + additionalParameters.put(IdTokenClaimNames.NONCE, nonceHash); + } + + private String createHash(String nonce) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(nonce.getBytes(StandardCharsets.US_ASCII)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java index 665755e58a..315f485a3b 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java @@ -24,10 +24,8 @@ import org.springframework.security.oauth2.client.registration.InMemoryClientReg 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.endpoint.OAuth2AuthorizationRequest; -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.endpoint.*; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -37,6 +35,7 @@ import static org.assertj.core.api.Assertions.entry; * Tests for {@link DefaultOAuth2AuthorizationRequestResolver}. * * @author Joe Grandja + * @author Mark Heckler */ public class DefaultOAuth2AuthorizationRequestResolverTests { private ClientRegistration registration1; @@ -119,12 +118,14 @@ public class DefaultOAuth2AuthorizationRequestResolverTests { assertThat(authorizationRequest.getState()).isNotNull(); assertThat(authorizationRequest.getAdditionalParameters()).doesNotContainKey(OAuth2ParameterNames.REGISTRATION_ID); assertThat(authorizationRequest.getAttributes()) - .containsExactly(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId())); + .containsOnlyKeys(OAuth2ParameterNames.REGISTRATION_ID, IdTokenClaimNames.NONCE) + .contains(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId())); assertThat(authorizationRequest.getAuthorizationRequestUri()) .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + "scope=read:user&state=.{15,}&" + - "redirect_uri=http://localhost/login/oauth2/code/registration-id"); + "redirect_uri=http://localhost/login/oauth2/code/registration-id&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } @Test @@ -137,7 +138,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests { OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request, clientRegistration.getRegistrationId()); assertThat(authorizationRequest).isNotNull(); assertThat(authorizationRequest.getAttributes()) - .containsExactly(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId())); + .containsOnlyKeys(OAuth2ParameterNames.REGISTRATION_ID, IdTokenClaimNames.NONCE) + .contains(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId())); } @Test @@ -259,7 +261,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests { .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + "scope=read:user&state=.{15,}&" + - "redirect_uri=http://localhost/login/oauth2/code/registration-id"); + "redirect_uri=http://localhost/login/oauth2/code/registration-id&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } @Test @@ -277,7 +280,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests { .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + "scope=read:user&state=.{15,}&" + - "redirect_uri=https://example.com/login/oauth2/code/registration-id"); + "redirect_uri=https://example.com/login/oauth2/code/registration-id&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } @Test @@ -292,7 +296,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests { .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + "scope=read:user&state=.{15,}&" + - "redirect_uri=http://localhost/authorize/oauth2/code/registration-id"); + "redirect_uri=http://localhost/authorize/oauth2/code/registration-id&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } @Test @@ -307,7 +312,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests { .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id-2&" + "scope=read:user&state=.{15,}&" + - "redirect_uri=http://localhost/login/oauth2/code/registration-id-2"); + "redirect_uri=http://localhost/login/oauth2/code/registration-id-2&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } @Test @@ -323,7 +329,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests { .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + "scope=read:user&state=.{15,}&" + - "redirect_uri=http://localhost/authorize/oauth2/code/registration-id"); + "redirect_uri=http://localhost/authorize/oauth2/code/registration-id&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } @Test @@ -339,7 +346,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests { .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id-2&" + "scope=read:user&state=.{15,}&" + - "redirect_uri=http://localhost/login/oauth2/code/registration-id-2"); + "redirect_uri=http://localhost/login/oauth2/code/registration-id-2&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } @Test @@ -375,6 +383,7 @@ public class DefaultOAuth2AuthorizationRequestResolverTests { "scope=read:user&state=.{15,}&" + "redirect_uri=http://localhost/login/oauth2/code/pkce-client-registration-id&" + "code_challenge_method=S256&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java index bbc47c9a56..4343ec9a00 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -49,6 +49,7 @@ import static org.mockito.Mockito.*; * Tests for {@link OAuth2AuthorizationRequestRedirectFilter}. * * @author Joe Grandja + * @author Mark Heckler */ public class OAuth2AuthorizationRequestRedirectFilterTests { private ClientRegistration registration1; @@ -154,7 +155,8 @@ public class OAuth2AuthorizationRequestRedirectFilterTests { assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + "scope=read:user&state=.{15,}&" + - "redirect_uri=http://localhost/login/oauth2/code/registration-id"); + "redirect_uri=http://localhost/login/oauth2/code/registration-id&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } @Test @@ -234,7 +236,8 @@ public class OAuth2AuthorizationRequestRedirectFilterTests { assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + "scope=read:user&state=.{15,}&" + - "redirect_uri=http://localhost/login/oauth2/code/registration-id"); + "redirect_uri=http://localhost/login/oauth2/code/registration-id&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } @Test @@ -255,7 +258,8 @@ public class OAuth2AuthorizationRequestRedirectFilterTests { assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + "scope=read:user&state=.{15,}&" + - "redirect_uri=http://localhost/authorize/oauth2/code/registration-id"); + "redirect_uri=http://localhost/authorize/oauth2/code/registration-id&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); verify(this.requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class)); } @@ -359,6 +363,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests { "response_type=code&client_id=client-id&" + "scope=read:user&state=.{15,}&" + "redirect_uri=http://localhost/login/oauth2/code/registration-id&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "login_hint=user@provider\\.com"); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java index 86a722d18c..60a797ea30 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java @@ -41,6 +41,7 @@ import static org.mockito.Mockito.when; /** * @author Rob Winch + * @author Mark Heckler * @since 5.1 */ @RunWith(MockitoJUnitRunner.class) @@ -82,7 +83,8 @@ public class DefaultServerOAuth2AuthorizationRequestResolverTests { assertThat(request.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + "scope=read:user&state=.*?&" + - "redirect_uri=/login/oauth2/code/registration-id"); + "redirect_uri=/login/oauth2/code/registration-id&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } private OAuth2AuthorizationRequest resolve(String path) { @@ -101,7 +103,8 @@ public class DefaultServerOAuth2AuthorizationRequestResolverTests { assertThat(request.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + "scope=read:user&state=.*?&" + - "redirect_uri=/login/oauth2/code/registration-id"); + "redirect_uri=/login/oauth2/code/registration-id&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } @Test @@ -121,6 +124,7 @@ public class DefaultServerOAuth2AuthorizationRequestResolverTests { "scope=read:user&state=.*?&" + "redirect_uri=/login/oauth2/code/registration-id&" + "code_challenge_method=S256&" + + "nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" + "code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}"); } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/endpoint/OidcParameterNames.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/endpoint/OidcParameterNames.java index 13c37441c8..77f5dbd7e9 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/endpoint/OidcParameterNames.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/endpoint/OidcParameterNames.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -20,6 +20,7 @@ package org.springframework.security.oauth2.core.oidc.endpoint; * and used by the authorization endpoint and token endpoint. * * @author Joe Grandja + * @author Mark Heckler * @since 5.0 * @see 18.2 OAuth Parameters Registration */ @@ -30,4 +31,9 @@ public interface OidcParameterNames { */ String ID_TOKEN = "id_token"; + /** + * {@code nonce} - used in the Access Token Request and Response. + */ + String NONCE = "nonce"; + }