Add support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
Closes gh-1813
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -512,6 +512,10 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
|
||||
if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(rs.getString("access_token_type"))) {
|
||||
tokenType = OAuth2AccessToken.TokenType.BEARER;
|
||||
}
|
||||
else if (OAuth2AccessToken.TokenType.DPOP.getValue()
|
||||
.equalsIgnoreCase(rs.getString("access_token_type"))) {
|
||||
tokenType = OAuth2AccessToken.TokenType.DPOP;
|
||||
}
|
||||
|
||||
Set<String> scopes = Collections.emptySet();
|
||||
String accessTokenScopes = rs.getString("access_token_scopes");
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2020-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.jwt.DPoPProofContext;
|
||||
import org.springframework.security.oauth2.jwt.DPoPProofJwtDecoderFactory;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 1.5
|
||||
*/
|
||||
final class DPoPProofVerifier {
|
||||
|
||||
private static final JwtDecoderFactory<DPoPProofContext> dPoPProofVerifierFactory = new DPoPProofJwtDecoderFactory();
|
||||
|
||||
private DPoPProofVerifier() {
|
||||
}
|
||||
|
||||
static Jwt verifyIfAvailable(OAuth2AuthorizationGrantAuthenticationToken authorizationGrantAuthentication) {
|
||||
String dPoPProof = (String) authorizationGrantAuthentication.getAdditionalParameters().get("dpop_proof");
|
||||
if (!StringUtils.hasText(dPoPProof)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String method = (String) authorizationGrantAuthentication.getAdditionalParameters().get("dpop_method");
|
||||
String targetUri = (String) authorizationGrantAuthentication.getAdditionalParameters().get("dpop_target_uri");
|
||||
|
||||
Jwt dPoPProofJwt;
|
||||
try {
|
||||
// @formatter:off
|
||||
DPoPProofContext dPoPProofContext = DPoPProofContext.withDPoPProof(dPoPProof)
|
||||
.method(method)
|
||||
.targetUri(targetUri)
|
||||
.build();
|
||||
// @formatter:on
|
||||
JwtDecoder dPoPProofVerifier = dPoPProofVerifierFactory.createDecoder(dPoPProofContext);
|
||||
dPoPProofJwt = dPoPProofVerifier.decode(dPoPProof);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF), ex);
|
||||
}
|
||||
|
||||
return dPoPProofJwt;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
@@ -25,6 +27,7 @@ import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Utility methods for the OAuth 2.0 {@link AuthenticationProvider}'s.
|
||||
@@ -51,8 +54,15 @@ final class OAuth2AuthenticationProviderUtils {
|
||||
static <T extends OAuth2Token> OAuth2AccessToken accessToken(OAuth2Authorization.Builder builder, T token,
|
||||
OAuth2TokenContext accessTokenContext) {
|
||||
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token.getTokenValue(),
|
||||
token.getIssuedAt(), token.getExpiresAt(), accessTokenContext.getAuthorizedScopes());
|
||||
OAuth2AccessToken.TokenType tokenType = OAuth2AccessToken.TokenType.BEARER;
|
||||
if (token instanceof ClaimAccessor claimAccessor) {
|
||||
Map<String, Object> cnfClaims = claimAccessor.getClaimAsMap("cnf");
|
||||
if (!CollectionUtils.isEmpty(cnfClaims) && cnfClaims.containsKey("jkt")) {
|
||||
tokenType = OAuth2AccessToken.TokenType.DPOP;
|
||||
}
|
||||
}
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, token.getTokenValue(), token.getIssuedAt(),
|
||||
token.getExpiresAt(), accessTokenContext.getAuthorizedScopes());
|
||||
OAuth2TokenFormat accessTokenFormat = accessTokenContext.getRegisteredClient()
|
||||
.getTokenSettings()
|
||||
.getAccessTokenFormat();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -185,6 +185,9 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
|
||||
// Verify the DPoP Proof (if available)
|
||||
Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(authorizationCodeAuthentication);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated token request parameters");
|
||||
}
|
||||
@@ -201,6 +204,9 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrant(authorizationCodeAuthentication);
|
||||
// @formatter:on
|
||||
if (dPoPProof != null) {
|
||||
tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
|
||||
}
|
||||
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -32,6 +32,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
@@ -116,21 +117,27 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
|
||||
|
||||
Set<String> authorizedScopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
|
||||
|
||||
// Verify the DPoP Proof (if available)
|
||||
Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(clientCredentialsAuthentication);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated token request parameters");
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(clientPrincipal)
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.authorizationGrant(clientCredentialsAuthentication)
|
||||
.build();
|
||||
.authorizationGrant(clientCredentialsAuthentication);
|
||||
// @formatter:on
|
||||
if (dPoPProof != null) {
|
||||
tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
|
||||
}
|
||||
OAuth2TokenContext tokenContext = tokenContextBuilder.build();
|
||||
|
||||
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (generatedAccessToken == null) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -34,6 +34,7 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2UserCode;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
@@ -182,6 +183,9 @@ public final class OAuth2DeviceCodeAuthenticationProvider implements Authenticat
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
// Verify the DPoP Proof (if available)
|
||||
Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(deviceCodeAuthentication);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated device token request parameters");
|
||||
}
|
||||
@@ -196,6 +200,9 @@ public final class OAuth2DeviceCodeAuthenticationProvider implements Authenticat
|
||||
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
||||
.authorizationGrant(deviceCodeAuthentication);
|
||||
// @formatter:on
|
||||
if (dPoPProof != null) {
|
||||
tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -157,6 +157,9 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
|
||||
}
|
||||
|
||||
// Verify the DPoP Proof (if available)
|
||||
Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(refreshTokenAuthentication);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated token request parameters");
|
||||
}
|
||||
@@ -175,6 +178,9 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
|
||||
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
|
||||
.authorizationGrant(refreshTokenAuthentication);
|
||||
// @formatter:on
|
||||
if (dPoPProof != null) {
|
||||
tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
|
||||
}
|
||||
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -38,6 +38,7 @@ import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
@@ -203,6 +204,9 @@ public final class OAuth2TokenExchangeAuthenticationProvider implements Authenti
|
||||
authorizedScopes = validateRequestedScopes(registeredClient, subjectAuthorization.getAuthorizedScopes());
|
||||
}
|
||||
|
||||
// Verify the DPoP Proof (if available)
|
||||
Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(tokenExchangeAuthentication);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated token request parameters");
|
||||
}
|
||||
@@ -220,6 +224,9 @@ public final class OAuth2TokenExchangeAuthenticationProvider implements Authenti
|
||||
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
|
||||
.authorizationGrant(tokenExchangeAuthentication);
|
||||
// @formatter:on
|
||||
if (dPoPProof != null) {
|
||||
tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
|
||||
}
|
||||
|
||||
// ----- Access token -----
|
||||
OAuth2TokenContext tokenContext = tokenContextBuilder.build();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
@@ -23,10 +24,14 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.nimbusds.jose.jwk.AsymmetricJWK;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor;
|
||||
@@ -36,6 +41,7 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
@@ -56,6 +62,8 @@ final class DefaultOAuth2TokenCustomizers {
|
||||
}
|
||||
|
||||
private static void customize(OAuth2TokenContext tokenContext, Map<String, Object> claims) {
|
||||
Map<String, Object> cnfClaims = null;
|
||||
|
||||
// Add 'cnf' claim for Mutual-TLS Client Certificate-Bound Access Tokens
|
||||
if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenContext.getTokenType())
|
||||
&& tokenContext.getAuthorizationGrant() != null && tokenContext.getAuthorizationGrant()
|
||||
@@ -69,9 +77,8 @@ final class DefaultOAuth2TokenCustomizers {
|
||||
X509Certificate[] clientCertificateChain = (X509Certificate[]) clientAuthentication.getCredentials();
|
||||
try {
|
||||
String sha256Thumbprint = computeSHA256Thumbprint(clientCertificateChain[0]);
|
||||
Map<String, Object> x5tClaim = new HashMap<>();
|
||||
x5tClaim.put("x5t#S256", sha256Thumbprint);
|
||||
claims.put("cnf", x5tClaim);
|
||||
cnfClaims = new HashMap<>();
|
||||
cnfClaims.put("x5t#S256", sha256Thumbprint);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
@@ -81,6 +88,44 @@ final class DefaultOAuth2TokenCustomizers {
|
||||
}
|
||||
}
|
||||
|
||||
// Add 'cnf' claim for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
|
||||
Jwt dPoPProofJwt = tokenContext.get(OAuth2TokenContext.DPOP_PROOF_KEY);
|
||||
if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenContext.getTokenType()) && dPoPProofJwt != null) {
|
||||
PublicKey publicKey = null;
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> jwkJson = (Map<String, Object>) dPoPProofJwt.getHeaders().get("jwk");
|
||||
try {
|
||||
JWK jwk = JWK.parse(jwkJson);
|
||||
if (jwk instanceof AsymmetricJWK) {
|
||||
publicKey = ((AsymmetricJWK) jwk).toPublicKey();
|
||||
}
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
}
|
||||
if (publicKey == null) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
|
||||
"jwk header is missing or invalid.", null);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
try {
|
||||
String sha256Thumbprint = computeSHA256Thumbprint(publicKey);
|
||||
if (cnfClaims == null) {
|
||||
cnfClaims = new HashMap<>();
|
||||
}
|
||||
cnfClaims.put("jkt", sha256Thumbprint);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"Failed to compute SHA-256 Thumbprint for DPoP Proof PublicKey.", null);
|
||||
throw new OAuth2AuthenticationException(error, ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (!CollectionUtils.isEmpty(cnfClaims)) {
|
||||
claims.put("cnf", cnfClaims);
|
||||
}
|
||||
|
||||
// Add 'act' claim for delegation use case of Token Exchange Grant.
|
||||
// If more than one actor is present, we create a chain of delegation by nesting
|
||||
// "act" claims.
|
||||
@@ -104,4 +149,10 @@ final class DefaultOAuth2TokenCustomizers {
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
}
|
||||
|
||||
private static String computeSHA256Thumbprint(PublicKey publicKey) throws Exception {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] digest = md.digest(publicKey.getEncoded());
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -175,6 +175,12 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
|
||||
jwtContextBuilder.put(SessionInformation.class, sessionInformation);
|
||||
}
|
||||
}
|
||||
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
|
||||
Jwt dPoPProofJwt = context.get(OAuth2TokenContext.DPOP_PROOF_KEY);
|
||||
if (dPoPProofJwt != null) {
|
||||
jwtContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProofJwt);
|
||||
}
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
JwtEncodingContext jwtContext = jwtContextBuilder.build();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -28,6 +28,7 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||
@@ -105,6 +106,12 @@ public final class OAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OA
|
||||
if (context.getAuthorizationGrant() != null) {
|
||||
accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());
|
||||
}
|
||||
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
|
||||
Jwt dPoPProofJwt = context.get(OAuth2TokenContext.DPOP_PROOF_KEY);
|
||||
if (dPoPProofJwt != null) {
|
||||
accessTokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProofJwt);
|
||||
}
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -24,6 +24,7 @@ import java.util.function.Consumer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
@@ -43,6 +44,11 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public interface OAuth2TokenContext extends Context {
|
||||
|
||||
/**
|
||||
* @since 1.5
|
||||
*/
|
||||
String DPOP_PROOF_KEY = Jwt.class.getName().concat(".DPOP_PROOF");
|
||||
|
||||
/**
|
||||
* Returns the {@link RegisteredClient registered client}.
|
||||
* @return the {@link RegisteredClient}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -83,6 +83,9 @@ public final class OAuth2AuthorizationCodeAuthenticationConverter implements Aut
|
||||
}
|
||||
});
|
||||
|
||||
// Validate DPoP Proof HTTP Header (if available)
|
||||
OAuth2EndpointUtils.validateAndAddDPoPParametersIfAvailable(request, additionalParameters);
|
||||
|
||||
return new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, redirectUri, additionalParameters);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -80,6 +80,9 @@ public final class OAuth2ClientCredentialsAuthenticationConverter implements Aut
|
||||
}
|
||||
});
|
||||
|
||||
// Validate DPoP Proof HTTP Header (if available)
|
||||
OAuth2EndpointUtils.validateAndAddDPoPParametersIfAvailable(request, additionalParameters);
|
||||
|
||||
return new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal, requestedScopes, additionalParameters);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -74,6 +74,9 @@ public final class OAuth2DeviceCodeAuthenticationConverter implements Authentica
|
||||
}
|
||||
});
|
||||
|
||||
// Validate DPoP Proof HTTP Header (if available)
|
||||
OAuth2EndpointUtils.validateAndAddDPoPParametersIfAvailable(request, additionalParameters);
|
||||
|
||||
return new OAuth2DeviceCodeAuthenticationToken(deviceCode, clientPrincipal, additionalParameters);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -23,8 +23,10 @@ import java.util.Map;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -104,6 +106,22 @@ final class OAuth2EndpointUtils {
|
||||
&& request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
|
||||
}
|
||||
|
||||
static void validateAndAddDPoPParametersIfAvailable(HttpServletRequest request,
|
||||
Map<String, Object> additionalParameters) {
|
||||
final String dPoPProofHeaderName = OAuth2AccessToken.TokenType.DPOP.getValue();
|
||||
String dPoPProof = request.getHeader(dPoPProofHeaderName);
|
||||
if (StringUtils.hasText(dPoPProof)) {
|
||||
if (Collections.list(request.getHeaders(dPoPProofHeaderName)).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, dPoPProofHeaderName, ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
else {
|
||||
additionalParameters.put("dpop_proof", dPoPProof);
|
||||
additionalParameters.put("dpop_method", request.getMethod());
|
||||
additionalParameters.put("dpop_target_uri", request.getRequestURL().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void throwError(String errorCode, String parameterName, String errorUri) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -88,6 +88,9 @@ public final class OAuth2RefreshTokenAuthenticationConverter implements Authenti
|
||||
}
|
||||
});
|
||||
|
||||
// Validate DPoP Proof HTTP Header (if available)
|
||||
OAuth2EndpointUtils.validateAndAddDPoPParametersIfAvailable(request, additionalParameters);
|
||||
|
||||
return new OAuth2RefreshTokenAuthenticationToken(refreshToken, clientPrincipal, requestedScopes,
|
||||
additionalParameters);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -176,6 +176,9 @@ public final class OAuth2TokenExchangeAuthenticationConverter implements Authent
|
||||
}
|
||||
});
|
||||
|
||||
// Validate DPoP Proof HTTP Header (if available)
|
||||
OAuth2EndpointUtils.validateAndAddDPoPParametersIfAvailable(request, additionalParameters);
|
||||
|
||||
return new OAuth2TokenExchangeAuthenticationToken(requestedTokenType, subjectToken, subjectTokenType,
|
||||
clientPrincipal, actorToken, actorTokenType, new LinkedHashSet<>(resources),
|
||||
new LinkedHashSet<>(audiences), requestedScopes, additionalParameters);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -30,7 +30,11 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -51,12 +55,15 @@ 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.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
@@ -114,6 +121,8 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||
|
||||
private OAuth2TokenGenerator<?> tokenGenerator;
|
||||
|
||||
private JwtEncoder dPoPProofJwtEncoder;
|
||||
|
||||
private SessionRegistry sessionRegistry;
|
||||
|
||||
private OAuth2AuthorizationCodeAuthenticationProvider authenticationProvider;
|
||||
@@ -137,6 +146,9 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||
return delegatingTokenGenerator.generate(context);
|
||||
}
|
||||
});
|
||||
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
|
||||
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
|
||||
this.dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
|
||||
this.sessionRegistry = mock(SessionRegistry.class);
|
||||
this.authenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(this.authorizationService,
|
||||
this.tokenGenerator);
|
||||
@@ -470,8 +482,12 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization
|
||||
.getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put("dpop_proof", generateDPoPProof("http://localhost/oauth2/token"));
|
||||
additionalParameters.put("dpop_method", "POST");
|
||||
additionalParameters.put("dpop_target_uri", "http://localhost/oauth2/token");
|
||||
OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
|
||||
AUTHORIZATION_CODE, clientPrincipal, authorizationRequest.getRedirectUri(), null);
|
||||
AUTHORIZATION_CODE, clientPrincipal, authorizationRequest.getRedirectUri(), additionalParameters);
|
||||
|
||||
given(this.jwtEncoder.encode(any())).willReturn(createJwt());
|
||||
|
||||
@@ -492,6 +508,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||
.isEqualTo(authentication);
|
||||
assertThat(jwtEncodingContext.getJwsHeader()).isNotNull();
|
||||
assertThat(jwtEncodingContext.getClaims()).isNotNull();
|
||||
assertThat(jwtEncodingContext.<Jwt>get(OAuth2TokenContext.DPOP_PROOF_KEY)).isNotNull();
|
||||
|
||||
ArgumentCaptor<JwtEncoderParameters> jwtEncoderParametersCaptor = ArgumentCaptor
|
||||
.forClass(JwtEncoderParameters.class);
|
||||
@@ -798,4 +815,24 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
}
|
||||
|
||||
private String generateDPoPProof(String tokenEndpointUri) {
|
||||
// @formatter:off
|
||||
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
|
||||
.toPublicJWK()
|
||||
.toJSONObject();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
|
||||
.type("dpop+jwt")
|
||||
.jwk(publicJwk)
|
||||
.build();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuedAt(Instant.now())
|
||||
.claim("htm", "POST")
|
||||
.claim("htu", tokenEndpointUri)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
Jwt jwt = this.dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
|
||||
return jwt.getTokenValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -18,9 +18,15 @@ package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -34,10 +40,15 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
@@ -84,6 +95,8 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests {
|
||||
|
||||
private OAuth2TokenGenerator<?> tokenGenerator;
|
||||
|
||||
private JwtEncoder dPoPProofJwtEncoder;
|
||||
|
||||
private OAuth2ClientCredentialsAuthenticationProvider authenticationProvider;
|
||||
|
||||
@BeforeEach
|
||||
@@ -104,6 +117,9 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests {
|
||||
return delegatingTokenGenerator.generate(context);
|
||||
}
|
||||
});
|
||||
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
|
||||
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
|
||||
this.dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
|
||||
this.authenticationProvider = new OAuth2ClientCredentialsAuthenticationProvider(this.authorizationService,
|
||||
this.tokenGenerator);
|
||||
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
|
||||
@@ -267,8 +283,12 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
|
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put("dpop_proof", generateDPoPProof("http://localhost/oauth2/token"));
|
||||
additionalParameters.put("dpop_method", "POST");
|
||||
additionalParameters.put("dpop_target_uri", "http://localhost/oauth2/token");
|
||||
OAuth2ClientCredentialsAuthenticationToken authentication = new OAuth2ClientCredentialsAuthenticationToken(
|
||||
clientPrincipal, null, null);
|
||||
clientPrincipal, null, additionalParameters);
|
||||
|
||||
given(this.jwtEncoder.encode(any())).willReturn(createJwt(registeredClient.getScopes()));
|
||||
|
||||
@@ -287,6 +307,7 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests {
|
||||
.isEqualTo(authentication);
|
||||
assertThat(jwtEncodingContext.getJwsHeader()).isNotNull();
|
||||
assertThat(jwtEncodingContext.getClaims()).isNotNull();
|
||||
assertThat(jwtEncodingContext.<Jwt>get(OAuth2TokenContext.DPOP_PROOF_KEY)).isNotNull();
|
||||
|
||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
||||
verify(this.authorizationService).save(authorizationCaptor.capture());
|
||||
@@ -352,4 +373,24 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests {
|
||||
.build();
|
||||
}
|
||||
|
||||
private String generateDPoPProof(String tokenEndpointUri) {
|
||||
// @formatter:off
|
||||
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
|
||||
.toPublicJWK()
|
||||
.toJSONObject();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
|
||||
.type("dpop+jwt")
|
||||
.jwk(publicJwk)
|
||||
.build();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuedAt(Instant.now())
|
||||
.claim("htm", "POST")
|
||||
.claim("htu", tokenEndpointUri)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
Jwt jwt = this.dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
|
||||
return jwt.getTokenValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -18,10 +18,15 @@ package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -38,6 +43,14 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2UserCode;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
@@ -81,6 +94,8 @@ public class OAuth2DeviceCodeAuthenticationProviderTests {
|
||||
|
||||
private OAuth2TokenGenerator<OAuth2Token> tokenGenerator;
|
||||
|
||||
private JwtEncoder dPoPProofJwtEncoder;
|
||||
|
||||
private OAuth2DeviceCodeAuthenticationProvider authenticationProvider;
|
||||
|
||||
@BeforeEach
|
||||
@@ -88,6 +103,9 @@ public class OAuth2DeviceCodeAuthenticationProviderTests {
|
||||
public void setUp() {
|
||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
this.tokenGenerator = mock(OAuth2TokenGenerator.class);
|
||||
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
|
||||
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
|
||||
this.dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
|
||||
this.authenticationProvider = new OAuth2DeviceCodeAuthenticationProvider(this.authorizationService,
|
||||
this.tokenGenerator);
|
||||
mockAuthorizationServerContext();
|
||||
@@ -352,7 +370,15 @@ public class OAuth2DeviceCodeAuthenticationProviderTests {
|
||||
@Test
|
||||
public void authenticateWhenValidDeviceCodeThenReturnAccessTokenAndRefreshToken() {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
Authentication authentication = createAuthentication(registeredClient);
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
|
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null);
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put("dpop_proof", generateDPoPProof("http://localhost/oauth2/token"));
|
||||
additionalParameters.put("dpop_method", "POST");
|
||||
additionalParameters.put("dpop_target_uri", "http://localhost/oauth2/token");
|
||||
OAuth2DeviceCodeAuthenticationToken authentication = new OAuth2DeviceCodeAuthenticationToken(DEVICE_CODE,
|
||||
clientPrincipal, additionalParameters);
|
||||
|
||||
// @formatter:off
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
|
||||
.token(createDeviceCode())
|
||||
@@ -396,6 +422,7 @@ public class OAuth2DeviceCodeAuthenticationProviderTests {
|
||||
assertThat(tokenContext.getAuthorizedScopes()).isEqualTo(authorization.getAuthorizedScopes());
|
||||
assertThat(tokenContext.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.DEVICE_CODE);
|
||||
assertThat(tokenContext.<Authentication>getAuthorizationGrant()).isEqualTo(authentication);
|
||||
assertThat(tokenContext.<Jwt>get(OAuth2TokenContext.DPOP_PROOF_KEY)).isNotNull();
|
||||
}
|
||||
assertThat(tokenContextCaptor.getAllValues().get(0).getTokenType()).isEqualTo(OAuth2TokenType.ACCESS_TOKEN);
|
||||
assertThat(tokenContextCaptor.getAllValues().get(1).getTokenType()).isEqualTo(OAuth2TokenType.REFRESH_TOKEN);
|
||||
@@ -448,4 +475,24 @@ public class OAuth2DeviceCodeAuthenticationProviderTests {
|
||||
return (token) -> token.getMetadata(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME);
|
||||
}
|
||||
|
||||
private String generateDPoPProof(String tokenEndpointUri) {
|
||||
// @formatter:off
|
||||
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
|
||||
.toPublicJWK()
|
||||
.toJSONObject();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
|
||||
.type("dpop+jwt")
|
||||
.jwk(publicJwk)
|
||||
.build();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuedAt(Instant.now())
|
||||
.claim("htm", "POST")
|
||||
.claim("htu", tokenEndpointUri)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
Jwt jwt = this.dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
|
||||
return jwt.getTokenValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -24,7 +24,11 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -43,10 +47,15 @@ import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
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.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
@@ -100,6 +109,8 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
|
||||
|
||||
private OAuth2TokenGenerator<?> tokenGenerator;
|
||||
|
||||
private JwtEncoder dPoPProofJwtEncoder;
|
||||
|
||||
private OAuth2RefreshTokenAuthenticationProvider authenticationProvider;
|
||||
|
||||
@BeforeEach
|
||||
@@ -122,6 +133,9 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
|
||||
return delegatingTokenGenerator.generate(context);
|
||||
}
|
||||
});
|
||||
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
|
||||
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
|
||||
this.dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
|
||||
this.authenticationProvider = new OAuth2RefreshTokenAuthenticationProvider(this.authorizationService,
|
||||
this.tokenGenerator);
|
||||
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
|
||||
@@ -171,8 +185,13 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
|
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put("dpop_proof", generateDPoPProof("http://localhost/oauth2/token"));
|
||||
additionalParameters.put("dpop_method", "POST");
|
||||
additionalParameters.put("dpop_target_uri", "http://localhost/oauth2/token");
|
||||
OAuth2RefreshTokenAuthenticationToken authentication = new OAuth2RefreshTokenAuthenticationToken(
|
||||
authorization.getRefreshToken().getToken().getTokenValue(), clientPrincipal, null, null);
|
||||
authorization.getRefreshToken().getToken().getTokenValue(), clientPrincipal, null,
|
||||
additionalParameters);
|
||||
|
||||
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) this.authenticationProvider
|
||||
.authenticate(authentication);
|
||||
@@ -191,6 +210,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
|
||||
.isEqualTo(authentication);
|
||||
assertThat(jwtEncodingContext.getJwsHeader()).isNotNull();
|
||||
assertThat(jwtEncodingContext.getClaims()).isNotNull();
|
||||
assertThat(jwtEncodingContext.<Jwt>get(OAuth2TokenContext.DPOP_PROOF_KEY)).isNotNull();
|
||||
|
||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
||||
verify(this.authorizationService).save(authorizationCaptor.capture());
|
||||
@@ -635,4 +655,24 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
|
||||
.build();
|
||||
}
|
||||
|
||||
private String generateDPoPProof(String tokenEndpointUri) {
|
||||
// @formatter:off
|
||||
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
|
||||
.toPublicJWK()
|
||||
.toJSONObject();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
|
||||
.type("dpop+jwt")
|
||||
.jwk(publicJwk)
|
||||
.build();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuedAt(Instant.now())
|
||||
.claim("htm", "POST")
|
||||
.claim("htu", tokenEndpointUri)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
Jwt jwt = this.dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
|
||||
return jwt.getTokenValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -18,11 +18,16 @@ package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -38,6 +43,14 @@ import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
@@ -88,6 +101,8 @@ public class OAuth2TokenExchangeAuthenticationProviderTests {
|
||||
|
||||
private OAuth2TokenGenerator<OAuth2Token> tokenGenerator;
|
||||
|
||||
private JwtEncoder dPoPProofJwtEncoder;
|
||||
|
||||
private OAuth2TokenExchangeAuthenticationProvider authenticationProvider;
|
||||
|
||||
@BeforeEach
|
||||
@@ -95,6 +110,9 @@ public class OAuth2TokenExchangeAuthenticationProviderTests {
|
||||
public void setUp() {
|
||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
this.tokenGenerator = mock(OAuth2TokenGenerator.class);
|
||||
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
|
||||
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
|
||||
this.dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
|
||||
this.authenticationProvider = new OAuth2TokenExchangeAuthenticationProvider(this.authorizationService,
|
||||
this.tokenGenerator);
|
||||
mockAuthorizationServerContext();
|
||||
@@ -506,7 +524,15 @@ public class OAuth2TokenExchangeAuthenticationProviderTests {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
|
||||
.build();
|
||||
OAuth2TokenExchangeAuthenticationToken authentication = createImpersonationRequest(registeredClient);
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
|
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null);
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put("dpop_proof", generateDPoPProof("http://localhost/oauth2/token"));
|
||||
additionalParameters.put("dpop_method", "POST");
|
||||
additionalParameters.put("dpop_target_uri", "http://localhost/oauth2/token");
|
||||
OAuth2TokenExchangeAuthenticationToken authentication = new OAuth2TokenExchangeAuthenticationToken(
|
||||
JWT_TOKEN_TYPE_VALUE, SUBJECT_TOKEN, ACCESS_TOKEN_TYPE_VALUE, clientPrincipal, null, null, RESOURCES,
|
||||
AUDIENCES, registeredClient.getScopes(), additionalParameters);
|
||||
TestingAuthenticationToken userPrincipal = new TestingAuthenticationToken("user", null, "ROLE_USER");
|
||||
// @formatter:off
|
||||
OAuth2Authorization subjectAuthorization = TestOAuth2Authorizations.authorization(registeredClient)
|
||||
@@ -544,6 +570,7 @@ public class OAuth2TokenExchangeAuthenticationProviderTests {
|
||||
assertThat(tokenContext.getTokenType()).isEqualTo(OAuth2TokenType.ACCESS_TOKEN);
|
||||
assertThat(tokenContext.<Authentication>getAuthorizationGrant()).isEqualTo(authentication);
|
||||
assertThat(tokenContext.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.TOKEN_EXCHANGE);
|
||||
assertThat(tokenContext.<Jwt>get(OAuth2TokenContext.DPOP_PROOF_KEY)).isNotNull();
|
||||
|
||||
OAuth2Authorization authorization = authorizationCaptor.getValue();
|
||||
assertThat(authorization.getPrincipalName()).isEqualTo(subjectAuthorization.getPrincipalName());
|
||||
@@ -738,4 +765,24 @@ public class OAuth2TokenExchangeAuthenticationProviderTests {
|
||||
return (metadata) -> metadata.put(OAuth2TokenFormat.class.getName(), tokenFormat.getValue());
|
||||
}
|
||||
|
||||
private String generateDPoPProof(String tokenEndpointUri) {
|
||||
// @formatter:off
|
||||
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
|
||||
.toPublicJWK()
|
||||
.toJSONObject();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
|
||||
.type("dpop+jwt")
|
||||
.jwk(publicJwk)
|
||||
.build();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuedAt(Instant.now())
|
||||
.claim("htm", "POST")
|
||||
.claim("htu", tokenEndpointUri)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
Jwt jwt = this.dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
|
||||
return jwt.getTokenValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -29,6 +29,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
@@ -72,6 +73,7 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
@@ -81,9 +83,13 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
|
||||
@@ -189,6 +195,8 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
|
||||
private static NimbusJwtEncoder jwtEncoder;
|
||||
|
||||
private static NimbusJwtEncoder dPoPProofJwtEncoder;
|
||||
|
||||
private static AuthorizationServerSettings authorizationServerSettings;
|
||||
|
||||
private static HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
|
||||
@@ -234,6 +242,9 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
jwtEncoder = new NimbusJwtEncoder(jwkSource);
|
||||
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
|
||||
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
|
||||
dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
|
||||
authorizationServerSettings = AuthorizationServerSettings.builder()
|
||||
.authorizationEndpoint("/test/authorize")
|
||||
.tokenEndpoint("/test/token")
|
||||
@@ -974,6 +985,53 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
assertThat(tokenContext.getAuthorizationServerContext().getIssuer()).isEqualTo(issuer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenTokenRequestWithDPoPProofThenReturnDPoPBoundAccessToken() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
this.registeredClientRepository.save(registeredClient);
|
||||
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI;
|
||||
String dPoPProof = generateDPoPProof(tokenEndpointUri);
|
||||
|
||||
this.mvc
|
||||
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getTokenRequestParameters(registeredClient, authorization))
|
||||
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient))
|
||||
.header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.DPOP.getValue()));
|
||||
|
||||
authorization = this.authorizationService.findById(authorization.getId());
|
||||
assertThat(authorization.getAccessToken().getClaims()).containsKey("cnf");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf");
|
||||
assertThat(cnfClaims).containsKey("jkt");
|
||||
}
|
||||
|
||||
private static String generateDPoPProof(String tokenEndpointUri) {
|
||||
// @formatter:off
|
||||
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
|
||||
.toPublicJWK()
|
||||
.toJSONObject();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
|
||||
.type("dpop+jwt")
|
||||
.jwk(publicJwk)
|
||||
.build();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuedAt(Instant.now())
|
||||
.claim("htm", "POST")
|
||||
.claim("htu", tokenEndpointUri)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
Jwt jwt = dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
|
||||
return jwt.getTokenValue();
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -22,6 +22,8 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
@@ -62,6 +64,12 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider;
|
||||
@@ -137,6 +145,8 @@ public class OAuth2ClientCredentialsGrantTests {
|
||||
|
||||
private static OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
|
||||
|
||||
private static NimbusJwtEncoder dPoPProofJwtEncoder;
|
||||
|
||||
private static AuthenticationConverter authenticationConverter;
|
||||
|
||||
private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;
|
||||
@@ -165,6 +175,9 @@ public class OAuth2ClientCredentialsGrantTests {
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
jwtCustomizer = mock(OAuth2TokenCustomizer.class);
|
||||
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
|
||||
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
|
||||
dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
|
||||
authenticationConverter = mock(AuthenticationConverter.class);
|
||||
authenticationConvertersConsumer = mock(Consumer.class);
|
||||
authenticationProvider = mock(AuthenticationProvider.class);
|
||||
@@ -461,6 +474,47 @@ public class OAuth2ClientCredentialsGrantTests {
|
||||
assertThat(jwtEncodingContext.getAuthorizationServerContext().getIssuer()).isEqualTo(issuer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenTokenRequestWithDPoPProofThenReturnDPoPBoundAccessToken() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
|
||||
this.registeredClientRepository.save(registeredClient);
|
||||
|
||||
String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI;
|
||||
String dPoPProof = generateDPoPProof(tokenEndpointUri);
|
||||
|
||||
this.mvc
|
||||
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.param(OAuth2ParameterNames.SCOPE, "scope1 scope2")
|
||||
.header(HttpHeaders.AUTHORIZATION,
|
||||
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret()))
|
||||
.header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.DPOP.getValue()));
|
||||
}
|
||||
|
||||
private static String generateDPoPProof(String tokenEndpointUri) {
|
||||
// @formatter:off
|
||||
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
|
||||
.toPublicJWK()
|
||||
.toJSONObject();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
|
||||
.type("dpop+jwt")
|
||||
.jwk(publicJwk)
|
||||
.build();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuedAt(Instant.now())
|
||||
.claim("htm", "POST")
|
||||
.claim("htu", tokenEndpointUri)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
Jwt jwt = dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
|
||||
return jwt.getTokenValue();
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.config.annotati
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -49,6 +50,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2DeviceCode;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.OAuth2UserCode;
|
||||
@@ -58,6 +60,12 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2DeviceAuthorizationResponseHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
@@ -113,6 +121,8 @@ public class OAuth2DeviceCodeGrantTests {
|
||||
|
||||
private static JWKSource<SecurityContext> jwkSource;
|
||||
|
||||
private static NimbusJwtEncoder dPoPProofJwtEncoder;
|
||||
|
||||
private static final HttpMessageConverter<OAuth2DeviceAuthorizationResponse> deviceAuthorizationResponseHttpMessageConverter = new OAuth2DeviceAuthorizationResponseHttpMessageConverter();
|
||||
|
||||
private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
|
||||
@@ -138,6 +148,9 @@ public class OAuth2DeviceCodeGrantTests {
|
||||
public static void init() {
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
|
||||
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
|
||||
dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
|
||||
// @formatter:off
|
||||
db = new EmbeddedDatabaseBuilder()
|
||||
.generateUniqueName(true)
|
||||
@@ -538,6 +551,82 @@ public class OAuth2DeviceCodeGrantTests {
|
||||
assertThat(accessTokenAuthorization).isEqualTo(updatedAuthorization);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenAccessTokenRequestWithDPoPProofThenReturnDPoPBoundAccessToken() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.registeredClientRepository.save(registeredClient);
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plusSeconds(300);
|
||||
// @formatter:off
|
||||
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.principalName(registeredClient.getClientId())
|
||||
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
||||
.token(new OAuth2DeviceCode(DEVICE_CODE, issuedAt, expiresAt))
|
||||
.token(new OAuth2UserCode(USER_CODE, issuedAt, expiresAt), withInvalidated())
|
||||
.authorizedScopes(registeredClient.getScopes())
|
||||
.attribute(Principal.class.getName(), new UsernamePasswordAuthenticationToken("user", null))
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
// @formatter:off
|
||||
OAuth2AuthorizationConsent authorizationConsent =
|
||||
OAuth2AuthorizationConsent.withId(registeredClient.getClientId(), "user")
|
||||
.scope(registeredClient.getScopes().iterator().next())
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.authorizationConsentService.save(authorizationConsent);
|
||||
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.DEVICE_CODE.getValue());
|
||||
parameters.set(OAuth2ParameterNames.DEVICE_CODE, DEVICE_CODE);
|
||||
|
||||
String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI;
|
||||
String dPoPProof = generateDPoPProof(tokenEndpointUri);
|
||||
|
||||
// @formatter:off
|
||||
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
.params(parameters)
|
||||
.headers(withClientAuth(registeredClient))
|
||||
.header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.DPOP.getValue()));
|
||||
// @formatter:on
|
||||
|
||||
authorization = this.authorizationService.findById(authorization.getId());
|
||||
assertThat(authorization.getAccessToken().getClaims()).containsKey("cnf");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf");
|
||||
assertThat(cnfClaims).containsKey("jkt");
|
||||
}
|
||||
|
||||
private static String generateDPoPProof(String tokenEndpointUri) {
|
||||
// @formatter:off
|
||||
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
|
||||
.toPublicJWK()
|
||||
.toJSONObject();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
|
||||
.type("dpop+jwt")
|
||||
.jwk(publicJwk)
|
||||
.build();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuedAt(Instant.now())
|
||||
.claim("htm", "POST")
|
||||
.claim("htu", tokenEndpointUri)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
Jwt jwt = dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
|
||||
return jwt.getTokenValue();
|
||||
}
|
||||
|
||||
private static HttpHeaders withClientAuth(RegisteredClient registeredClient) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -18,10 +18,13 @@ package org.springframework.security.oauth2.server.authorization.config.annotati
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
@@ -70,8 +73,13 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.TestKeys;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
@@ -126,6 +134,8 @@ public class OAuth2RefreshTokenGrantTests {
|
||||
|
||||
private static NimbusJwtDecoder jwtDecoder;
|
||||
|
||||
private static NimbusJwtEncoder dPoPProofJwtEncoder;
|
||||
|
||||
private static HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext();
|
||||
@@ -147,6 +157,9 @@ public class OAuth2RefreshTokenGrantTests {
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
jwtDecoder = NimbusJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY).build();
|
||||
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
|
||||
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
|
||||
dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
|
||||
db = new EmbeddedDatabaseBuilder().generateUniqueName(true)
|
||||
.setType(EmbeddedDatabaseType.HSQL)
|
||||
.setScriptEncoding("UTF-8")
|
||||
@@ -266,6 +279,54 @@ public class OAuth2RefreshTokenGrantTests {
|
||||
.andExpect(jsonPath("$.scope").isNotEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenRefreshTokenRequestWithDPoPProofThenReturnDPoPBoundAccessToken() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
this.registeredClientRepository.save(registeredClient);
|
||||
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI;
|
||||
String dPoPProof = generateDPoPProof(tokenEndpointUri);
|
||||
|
||||
this.mvc
|
||||
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getRefreshTokenRequestParameters(authorization))
|
||||
.header(HttpHeaders.AUTHORIZATION,
|
||||
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret()))
|
||||
.header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.DPOP.getValue()));
|
||||
|
||||
authorization = this.authorizationService.findById(authorization.getId());
|
||||
assertThat(authorization.getAccessToken().getClaims()).containsKey("cnf");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> cnfClaims = (Map<String, Object>) authorization.getAccessToken().getClaims().get("cnf");
|
||||
assertThat(cnfClaims).containsKey("jkt");
|
||||
}
|
||||
|
||||
private static String generateDPoPProof(String tokenEndpointUri) {
|
||||
// @formatter:off
|
||||
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
|
||||
.toPublicJWK()
|
||||
.toJSONObject();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
|
||||
.type("dpop+jwt")
|
||||
.jwk(publicJwk)
|
||||
.build();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuedAt(Instant.now())
|
||||
.claim("htm", "POST")
|
||||
.claim("htu", tokenEndpointUri)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
Jwt jwt = dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
|
||||
return jwt.getTokenValue();
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> getRefreshTokenRequestParameters(OAuth2Authorization authorization) {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.REFRESH_TOKEN.getValue());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* Copyright 2020-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -60,6 +60,12 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenRespon
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
@@ -109,6 +115,8 @@ public class OAuth2TokenExchangeGrantTests {
|
||||
|
||||
private static final String JWT_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:jwt";
|
||||
|
||||
private static NimbusJwtEncoder dPoPProofJwtEncoder;
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext();
|
||||
|
||||
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
|
||||
@@ -129,6 +137,9 @@ public class OAuth2TokenExchangeGrantTests {
|
||||
public static void init() {
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
AuthorizationServerConfiguration.JWK_SOURCE = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
|
||||
JWKSet clientJwkSet = new JWKSet(TestJwks.DEFAULT_EC_JWK);
|
||||
JWKSource<SecurityContext> clientJwkSource = (jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet);
|
||||
dPoPProofJwtEncoder = new NimbusJwtEncoder(clientJwkSource);
|
||||
// @formatter:off
|
||||
AuthorizationServerConfiguration.DB = new EmbeddedDatabaseBuilder()
|
||||
.generateUniqueName(true)
|
||||
@@ -313,6 +324,63 @@ public class OAuth2TokenExchangeGrantTests {
|
||||
.isInstanceOf(OAuth2TokenExchangeCompositeAuthenticationToken.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenAccessTokenRequestWithDPoPProofThenReturnDPoPBoundAccessToken() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
|
||||
.build();
|
||||
this.registeredClientRepository.save(registeredClient);
|
||||
|
||||
UsernamePasswordAuthenticationToken userPrincipal = createUserPrincipal("user");
|
||||
UsernamePasswordAuthenticationToken adminPrincipal = createUserPrincipal("admin");
|
||||
Map<String, Object> actorTokenClaims = new HashMap<>();
|
||||
actorTokenClaims.put(OAuth2TokenClaimNames.ISS, "issuer2");
|
||||
actorTokenClaims.put(OAuth2TokenClaimNames.SUB, "admin");
|
||||
Map<String, Object> subjectTokenClaims = new HashMap<>();
|
||||
subjectTokenClaims.put(OAuth2TokenClaimNames.ISS, "issuer1");
|
||||
subjectTokenClaims.put(OAuth2TokenClaimNames.SUB, "user");
|
||||
subjectTokenClaims.put("may_act", actorTokenClaims);
|
||||
OAuth2AccessToken subjectToken = createAccessToken(SUBJECT_TOKEN);
|
||||
OAuth2AccessToken actorToken = createAccessToken(ACTOR_TOKEN);
|
||||
// @formatter:off
|
||||
OAuth2Authorization subjectAuthorization = TestOAuth2Authorizations.authorization(registeredClient, subjectToken, subjectTokenClaims)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.attribute(Principal.class.getName(), userPrincipal)
|
||||
.build();
|
||||
OAuth2Authorization actorAuthorization = TestOAuth2Authorizations.authorization(registeredClient, actorToken, actorTokenClaims)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.attribute(Principal.class.getName(), adminPrincipal)
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.authorizationService.save(subjectAuthorization);
|
||||
this.authorizationService.save(actorAuthorization);
|
||||
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.TOKEN_EXCHANGE.getValue());
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
||||
parameters.set(OAuth2ParameterNames.REQUESTED_TOKEN_TYPE, JWT_TOKEN_TYPE_VALUE);
|
||||
parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN, SUBJECT_TOKEN);
|
||||
parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN_TYPE, JWT_TOKEN_TYPE_VALUE);
|
||||
parameters.set(OAuth2ParameterNames.ACTOR_TOKEN, ACTOR_TOKEN);
|
||||
parameters.set(OAuth2ParameterNames.ACTOR_TOKEN_TYPE, ACCESS_TOKEN_TYPE_VALUE);
|
||||
parameters.set(OAuth2ParameterNames.SCOPE,
|
||||
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
|
||||
|
||||
String tokenEndpointUri = "http://localhost" + DEFAULT_TOKEN_ENDPOINT_URI;
|
||||
String dPoPProof = generateDPoPProof(tokenEndpointUri);
|
||||
|
||||
// @formatter:off
|
||||
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
.params(parameters)
|
||||
.headers(withClientAuth(registeredClient))
|
||||
.header(OAuth2AccessToken.TokenType.DPOP.getValue(), dPoPProof))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.DPOP.getValue()));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static OAuth2AccessToken createAccessToken(String tokenValue) {
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plusSeconds(300);
|
||||
@@ -338,6 +406,26 @@ public class OAuth2TokenExchangeGrantTests {
|
||||
return (token) -> token.getMetadata(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME);
|
||||
}
|
||||
|
||||
private static String generateDPoPProof(String tokenEndpointUri) {
|
||||
// @formatter:off
|
||||
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
|
||||
.toPublicJWK()
|
||||
.toJSONObject();
|
||||
JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
|
||||
.type("dpop+jwt")
|
||||
.jwk(publicJwk)
|
||||
.build();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuedAt(Instant.now())
|
||||
.claim("htm", "POST")
|
||||
.claim("htu", tokenEndpointUri)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
Jwt jwt = dPoPProofJwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));
|
||||
return jwt.getTokenValue();
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfiguration {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2025 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.web;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -67,7 +68,6 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
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.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -236,6 +236,17 @@ public class OAuth2TokenEndpointFilterTests {
|
||||
OAuth2ErrorCodes.INVALID_REQUEST, request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenTokenRequestMultipleDPoPHeaderThenInvalidRequestError() throws Exception {
|
||||
MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
|
||||
TestRegisteredClients.registeredClient().build());
|
||||
request.addHeader(OAuth2AccessToken.TokenType.DPOP.getValue(), "dpop-proof-jwt");
|
||||
request.addHeader(OAuth2AccessToken.TokenType.DPOP.getValue(), "dpop-proof-jwt-2");
|
||||
|
||||
doFilterWhenTokenRequestInvalidParameterThenError(OAuth2AccessToken.TokenType.DPOP.getValue(),
|
||||
OAuth2ErrorCodes.INVALID_REQUEST, request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationCodeTokenRequestThenAccessTokenResponse() throws Exception {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
@@ -256,6 +267,7 @@ public class OAuth2TokenEndpointFilterTests {
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
|
||||
MockHttpServletRequest request = createAuthorizationCodeTokenRequest(registeredClient);
|
||||
request.addHeader(OAuth2AccessToken.TokenType.DPOP.getValue(), "dpop-proof-jwt");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
@@ -274,9 +286,14 @@ public class OAuth2TokenEndpointFilterTests {
|
||||
assertThat(authorizationCodeAuthentication.getPrincipal()).isEqualTo(clientPrincipal);
|
||||
assertThat(authorizationCodeAuthentication.getRedirectUri())
|
||||
.isEqualTo(request.getParameter(OAuth2ParameterNames.REDIRECT_URI));
|
||||
assertThat(authorizationCodeAuthentication.getAdditionalParameters()).containsExactly(
|
||||
entry("custom-param-1", "custom-value-1"),
|
||||
entry("custom-param-2", new String[] { "custom-value-1", "custom-value-2" }));
|
||||
Map<String, Object> expectedAdditionalParameters = new HashMap<>();
|
||||
expectedAdditionalParameters.put("custom-param-1", "custom-value-1");
|
||||
expectedAdditionalParameters.put("custom-param-2", new String[] { "custom-value-1", "custom-value-2" });
|
||||
expectedAdditionalParameters.put("dpop_proof", "dpop-proof-jwt");
|
||||
expectedAdditionalParameters.put("dpop_method", "POST");
|
||||
expectedAdditionalParameters.put("dpop_target_uri", "http://localhost/oauth2/token");
|
||||
assertThat(authorizationCodeAuthentication.getAdditionalParameters())
|
||||
.containsExactlyInAnyOrderEntriesOf(expectedAdditionalParameters);
|
||||
assertThat(authorizationCodeAuthentication.getDetails())
|
||||
.asInstanceOf(InstanceOfAssertFactories.type(WebAuthenticationDetails.class))
|
||||
.extracting(WebAuthenticationDetails::getRemoteAddress)
|
||||
@@ -324,6 +341,7 @@ public class OAuth2TokenEndpointFilterTests {
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
|
||||
MockHttpServletRequest request = createClientCredentialsTokenRequest(registeredClient);
|
||||
request.addHeader(OAuth2AccessToken.TokenType.DPOP.getValue(), "dpop-proof-jwt");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
@@ -339,9 +357,14 @@ public class OAuth2TokenEndpointFilterTests {
|
||||
.getValue();
|
||||
assertThat(clientCredentialsAuthentication.getPrincipal()).isEqualTo(clientPrincipal);
|
||||
assertThat(clientCredentialsAuthentication.getScopes()).isEqualTo(registeredClient.getScopes());
|
||||
assertThat(clientCredentialsAuthentication.getAdditionalParameters()).containsExactly(
|
||||
entry("custom-param-1", "custom-value-1"),
|
||||
entry("custom-param-2", new String[] { "custom-value-1", "custom-value-2" }));
|
||||
Map<String, Object> expectedAdditionalParameters = new HashMap<>();
|
||||
expectedAdditionalParameters.put("custom-param-1", "custom-value-1");
|
||||
expectedAdditionalParameters.put("custom-param-2", new String[] { "custom-value-1", "custom-value-2" });
|
||||
expectedAdditionalParameters.put("dpop_proof", "dpop-proof-jwt");
|
||||
expectedAdditionalParameters.put("dpop_method", "POST");
|
||||
expectedAdditionalParameters.put("dpop_target_uri", "http://localhost/oauth2/token");
|
||||
assertThat(clientCredentialsAuthentication.getAdditionalParameters())
|
||||
.containsExactlyInAnyOrderEntriesOf(expectedAdditionalParameters);
|
||||
assertThat(clientCredentialsAuthentication.getDetails())
|
||||
.asInstanceOf(InstanceOfAssertFactories.type(WebAuthenticationDetails.class))
|
||||
.extracting(WebAuthenticationDetails::getRemoteAddress)
|
||||
@@ -412,6 +435,7 @@ public class OAuth2TokenEndpointFilterTests {
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
|
||||
MockHttpServletRequest request = createRefreshTokenTokenRequest(registeredClient);
|
||||
request.addHeader(OAuth2AccessToken.TokenType.DPOP.getValue(), "dpop-proof-jwt");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
@@ -428,9 +452,14 @@ public class OAuth2TokenEndpointFilterTests {
|
||||
assertThat(refreshTokenAuthenticationToken.getRefreshToken()).isEqualTo(refreshToken.getTokenValue());
|
||||
assertThat(refreshTokenAuthenticationToken.getPrincipal()).isEqualTo(clientPrincipal);
|
||||
assertThat(refreshTokenAuthenticationToken.getScopes()).isEqualTo(registeredClient.getScopes());
|
||||
assertThat(refreshTokenAuthenticationToken.getAdditionalParameters()).containsExactly(
|
||||
entry("custom-param-1", "custom-value-1"),
|
||||
entry("custom-param-2", new String[] { "custom-value-1", "custom-value-2" }));
|
||||
Map<String, Object> expectedAdditionalParameters = new HashMap<>();
|
||||
expectedAdditionalParameters.put("custom-param-1", "custom-value-1");
|
||||
expectedAdditionalParameters.put("custom-param-2", new String[] { "custom-value-1", "custom-value-2" });
|
||||
expectedAdditionalParameters.put("dpop_proof", "dpop-proof-jwt");
|
||||
expectedAdditionalParameters.put("dpop_method", "POST");
|
||||
expectedAdditionalParameters.put("dpop_target_uri", "http://localhost/oauth2/token");
|
||||
assertThat(refreshTokenAuthenticationToken.getAdditionalParameters())
|
||||
.containsExactlyInAnyOrderEntriesOf(expectedAdditionalParameters);
|
||||
assertThat(refreshTokenAuthenticationToken.getDetails())
|
||||
.asInstanceOf(InstanceOfAssertFactories.type(WebAuthenticationDetails.class))
|
||||
.extracting(WebAuthenticationDetails::getRemoteAddress)
|
||||
@@ -473,6 +502,7 @@ public class OAuth2TokenEndpointFilterTests {
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
|
||||
MockHttpServletRequest request = createTokenExchangeTokenRequest(registeredClient);
|
||||
request.addHeader(OAuth2AccessToken.TokenType.DPOP.getValue(), "dpop-proof-jwt");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
@@ -490,9 +520,14 @@ public class OAuth2TokenEndpointFilterTests {
|
||||
assertThat(tokenExchangeAuthenticationToken.getSubjectTokenType()).isEqualTo(ACCESS_TOKEN_TYPE);
|
||||
assertThat(tokenExchangeAuthenticationToken.getPrincipal()).isEqualTo(clientPrincipal);
|
||||
assertThat(tokenExchangeAuthenticationToken.getScopes()).isEqualTo(registeredClient.getScopes());
|
||||
assertThat(tokenExchangeAuthenticationToken.getAdditionalParameters()).containsExactly(
|
||||
entry("custom-param-1", "custom-value-1"),
|
||||
entry("custom-param-2", new String[] { "custom-value-1", "custom-value-2" }));
|
||||
Map<String, Object> expectedAdditionalParameters = new HashMap<>();
|
||||
expectedAdditionalParameters.put("custom-param-1", "custom-value-1");
|
||||
expectedAdditionalParameters.put("custom-param-2", new String[] { "custom-value-1", "custom-value-2" });
|
||||
expectedAdditionalParameters.put("dpop_proof", "dpop-proof-jwt");
|
||||
expectedAdditionalParameters.put("dpop_method", "POST");
|
||||
expectedAdditionalParameters.put("dpop_target_uri", "http://localhost/oauth2/token");
|
||||
assertThat(tokenExchangeAuthenticationToken.getAdditionalParameters())
|
||||
.containsExactlyInAnyOrderEntriesOf(expectedAdditionalParameters);
|
||||
assertThat(tokenExchangeAuthenticationToken.getDetails())
|
||||
.asInstanceOf(InstanceOfAssertFactories.type(WebAuthenticationDetails.class))
|
||||
.extracting(WebAuthenticationDetails::getRemoteAddress)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2025 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,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.web.authentication;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@@ -28,6 +29,7 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
@@ -107,6 +109,7 @@ public class OAuth2DeviceCodeAuthenticationConverterTests {
|
||||
request.addParameter(OAuth2ParameterNames.DEVICE_CODE, DEVICE_CODE);
|
||||
request.addParameter("param-1", "value-1");
|
||||
request.addParameter("param-2", "value-1", "value-2");
|
||||
request.addHeader(OAuth2AccessToken.TokenType.DPOP.getValue(), "dpop-proof-jwt");
|
||||
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl();
|
||||
securityContext.setAuthentication(new TestingAuthenticationToken(CLIENT_ID, null));
|
||||
@@ -117,8 +120,14 @@ public class OAuth2DeviceCodeAuthenticationConverterTests {
|
||||
assertThat(authentication).isNotNull();
|
||||
assertThat(authentication.getDeviceCode()).isEqualTo(DEVICE_CODE);
|
||||
assertThat(authentication.getPrincipal()).isInstanceOf(TestingAuthenticationToken.class);
|
||||
assertThat(authentication.getAdditionalParameters()).containsExactly(Map.entry("param-1", "value-1"),
|
||||
Map.entry("param-2", new String[] { "value-1", "value-2" }));
|
||||
Map<String, Object> expectedAdditionalParameters = new HashMap<>();
|
||||
expectedAdditionalParameters.put("param-1", "value-1");
|
||||
expectedAdditionalParameters.put("param-2", new String[] { "value-1", "value-2" });
|
||||
expectedAdditionalParameters.put("dpop_proof", "dpop-proof-jwt");
|
||||
expectedAdditionalParameters.put("dpop_method", "POST");
|
||||
expectedAdditionalParameters.put("dpop_target_uri", "http://localhost/oauth2/token");
|
||||
assertThat(authentication.getAdditionalParameters())
|
||||
.containsExactlyInAnyOrderEntriesOf(expectedAdditionalParameters);
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest createRequest() {
|
||||
|
||||
Reference in New Issue
Block a user