Add support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)

Closes gh-1813
This commit is contained in:
Joe Grandja
2025-02-12 07:50:40 -05:00
parent b556d19bcf
commit 779d87a279
30 changed files with 879 additions and 57 deletions

View File

@@ -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");

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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);

View File

@@ -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();

View File

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

View File

@@ -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();

View File

@@ -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();

View File

@@ -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}

View File

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

View File

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

View File

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

View File

@@ -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);

View File

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

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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());

View File

@@ -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());

View File

@@ -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());

View File

@@ -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());

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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() {