Skip to content

Add nonce to OIDC Authentication Request #7337

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
* Tests for {@link OAuth2ClientConfigurer}.
*
* @author Joe Grandja
* @author Mark Heckler
*/
public class OAuth2ClientConfigurerTests {
private static ClientRegistrationRepository clientRegistrationRepository;
Expand Down Expand Up @@ -138,7 +139,8 @@ public void configureWhenAuthorizationCodeRequestThenRedirectForAuthorization()
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
Expand All @@ -151,7 +153,8 @@ public void configureWhenOauth2ClientInLambdaThenRedirectForAuthorization() thro
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
Expand Down Expand Up @@ -203,7 +206,8 @@ public void configureWhenRequestCacheProvidedAndClientAuthorizationRequiredExcep
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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
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;

Expand All @@ -61,6 +65,7 @@
* to complete the authentication.
*
* @author Joe Grandja
* @author Mark Heckler
* @since 5.0
* @see OAuth2LoginAuthenticationToken
* @see OAuth2AccessTokenResponseClient
Expand All @@ -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<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
private final OAuth2UserService<OidcUserRequest, OidcUser> userService;
private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new OidcIdTokenDecoderFactory();
Expand Down Expand Up @@ -152,7 +158,23 @@ public Authentication authenticate(Authentication authentication) throws Authent
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));
Expand Down Expand Up @@ -211,4 +233,10 @@ private OidcIdToken createOidcToken(ClientRegistration clientRegistration, OAuth
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@
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.
*
* <p>
* 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.
Expand All @@ -61,7 +65,6 @@
* to complete the authentication.
*
* @author Rob Winch
* @since 5.1
* @see OAuth2LoginAuthenticationToken
* @see ReactiveOAuth2AccessTokenResponseClient
* @see ReactiveOAuth2UserService
Expand All @@ -70,13 +73,15 @@
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
* @since 5.1
*/
public class OidcAuthorizationCodeReactiveAuthenticationManager implements
ReactiveAuthenticationManager {

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<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;

Expand Down Expand Up @@ -148,8 +153,8 @@ public Mono<Authentication> authenticate(Authentication authentication) {
* Sets the {@link ReactiveJwtDecoderFactory} used for {@link OidcIdToken} signature verification.
* The factory returns a {@link ReactiveJwtDecoder} associated to the provided {@link ClientRegistration}.
*
* @since 5.2
* @param jwtDecoderFactory the {@link ReactiveJwtDecoderFactory} used for {@link OidcIdToken} signature verification
* @since 5.2
*/
public final void setJwtDecoderFactory(ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory) {
Assert.notNull(jwtDecoderFactory, "jwtDecoderFactory cannot be null");
Expand All @@ -170,7 +175,8 @@ private Mono<OAuth2LoginAuthenticationToken> authenticationResult(OAuth2Authoriz
}

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<? extends GrantedAuthority> mappedAuthorities =
Expand All @@ -192,4 +198,33 @@ private Mono<OidcIdToken> createOidcToken(ClientRegistration clientRegistration,
return jwtDecoder.decode(rawIdToken)
.map(jwt -> new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()));
}

private Mono<OidcIdToken> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
*
* @author Joe Grandja
* @author Rafael Dominguez
* @author Mark Heckler
* @since 5.2
* @see JwtDecoderFactory
* @see ClientRegistration
Expand Down Expand Up @@ -88,12 +89,14 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
Converter<Object, ?> urlConverter = getConverter(TypeDescriptor.valueOf(URL.class));
Converter<Object, ?> stringConverter = getConverter(TypeDescriptor.valueOf(String.class));
Converter<Object, ?> collectionStringConverter = getConverter(
TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class)));

Map<String, Converter<Object, ?>> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
*
* @author Rob Winch
* @author Joe Grandja
* @author Mark Heckler
* @since 5.1
* @see OAuth2TokenValidator
* @see Jwt
Expand Down Expand Up @@ -107,13 +108,6 @@ public OAuth2TokenValidatorResult validate(Jwt idToken) {
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));
}
Expand Down Expand Up @@ -167,4 +161,5 @@ private static Map<String, Object> validateRequiredClaims(Jwt idToken) {

return requiredClaims;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
*
* @author Joe Grandja
* @author Rafael Dominguez
* @author Mark Heckler
* @since 5.2
* @see ReactiveJwtDecoderFactory
* @see ClientRegistration
Expand Down Expand Up @@ -88,12 +89,14 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
Converter<Object, ?> urlConverter = getConverter(TypeDescriptor.valueOf(URL.class));
Converter<Object, ?> stringConverter = getConverter(TypeDescriptor.valueOf(String.class));
Converter<Object, ?> collectionStringConverter = getConverter(
TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class)));

Map<String, Converter<Object, ?>> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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;
Expand Down Expand Up @@ -51,6 +52,7 @@
* @author Joe Grandja
* @author Rob Winch
* @author Eddú Meléndez
* @author Mark Heckler
* @since 5.1
* @see OAuth2AuthorizationRequestResolver
* @see OAuth2AuthorizationRequestRedirectFilter
Expand All @@ -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.
Expand Down Expand Up @@ -118,11 +120,15 @@ private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String re
OAuth2AuthorizationRequest.Builder builder;
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
builder = OAuth2AuthorizationRequest.authorizationCode();
Map<String, Object> additionalParameters = new HashMap<>();

addNonceParameters(attributes, additionalParameters);

if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
Map<String, Object> additionalParameters = new HashMap<>();
addPkceParameters(attributes, additionalParameters);
builder.additionalParameters(additionalParameters);
}

builder.additionalParameters(additionalParameters);
} else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
builder = OAuth2AuthorizationRequest.implicit();
} else {
Expand Down Expand Up @@ -201,6 +207,27 @@ private static String expandRedirectUri(HttpServletRequest request, ClientRegist
.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 <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes">15.5.2. Nonce Implementation Notes</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation">3.1.3.7. ID Token Validation</a>
*/
private void addNonceParameters(Map<String, Object> attributes, Map<String, Object> 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
*
Expand All @@ -214,20 +241,20 @@ private static String expandRedirectUri(HttpServletRequest request, ClientRegist
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a>
*/
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> 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) {
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
}
}

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);
}
}
Loading