Skip to content

Commit 779d87a

Browse files
committed
Add support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
Closes gh-1813
1 parent b556d19 commit 779d87a

File tree

30 files changed

+879
-57
lines changed

30 files changed

+879
-57
lines changed

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -512,6 +512,10 @@ public OAuth2Authorization mapRow(ResultSet rs, int rowNum) throws SQLException
512512
if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(rs.getString("access_token_type"))) {
513513
tokenType = OAuth2AccessToken.TokenType.BEARER;
514514
}
515+
else if (OAuth2AccessToken.TokenType.DPOP.getValue()
516+
.equalsIgnoreCase(rs.getString("access_token_type"))) {
517+
tokenType = OAuth2AccessToken.TokenType.DPOP;
518+
}
515519

516520
Set<String> scopes = Collections.emptySet();
517521
String accessTokenScopes = rs.getString("access_token_scopes");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.server.authorization.authentication;
17+
18+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
19+
import org.springframework.security.oauth2.core.OAuth2Error;
20+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
21+
import org.springframework.security.oauth2.jwt.DPoPProofContext;
22+
import org.springframework.security.oauth2.jwt.DPoPProofJwtDecoderFactory;
23+
import org.springframework.security.oauth2.jwt.Jwt;
24+
import org.springframework.security.oauth2.jwt.JwtDecoder;
25+
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
26+
import org.springframework.util.StringUtils;
27+
28+
/**
29+
* @author Joe Grandja
30+
* @since 1.5
31+
*/
32+
final class DPoPProofVerifier {
33+
34+
private static final JwtDecoderFactory<DPoPProofContext> dPoPProofVerifierFactory = new DPoPProofJwtDecoderFactory();
35+
36+
private DPoPProofVerifier() {
37+
}
38+
39+
static Jwt verifyIfAvailable(OAuth2AuthorizationGrantAuthenticationToken authorizationGrantAuthentication) {
40+
String dPoPProof = (String) authorizationGrantAuthentication.getAdditionalParameters().get("dpop_proof");
41+
if (!StringUtils.hasText(dPoPProof)) {
42+
return null;
43+
}
44+
45+
String method = (String) authorizationGrantAuthentication.getAdditionalParameters().get("dpop_method");
46+
String targetUri = (String) authorizationGrantAuthentication.getAdditionalParameters().get("dpop_target_uri");
47+
48+
Jwt dPoPProofJwt;
49+
try {
50+
// @formatter:off
51+
DPoPProofContext dPoPProofContext = DPoPProofContext.withDPoPProof(dPoPProof)
52+
.method(method)
53+
.targetUri(targetUri)
54+
.build();
55+
// @formatter:on
56+
JwtDecoder dPoPProofVerifier = dPoPProofVerifierFactory.createDecoder(dPoPProofContext);
57+
dPoPProofJwt = dPoPProofVerifier.decode(dPoPProof);
58+
}
59+
catch (Exception ex) {
60+
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF), ex);
61+
}
62+
63+
return dPoPProofJwt;
64+
}
65+
66+
}

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.security.oauth2.server.authorization.authentication;
1717

18+
import java.util.Map;
19+
1820
import org.springframework.security.authentication.AuthenticationProvider;
1921
import org.springframework.security.core.Authentication;
2022
import org.springframework.security.oauth2.core.ClaimAccessor;
@@ -25,6 +27,7 @@
2527
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
2628
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
2729
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
30+
import org.springframework.util.CollectionUtils;
2831

2932
/**
3033
* Utility methods for the OAuth 2.0 {@link AuthenticationProvider}'s.
@@ -51,8 +54,15 @@ static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidCli
5154
static <T extends OAuth2Token> OAuth2AccessToken accessToken(OAuth2Authorization.Builder builder, T token,
5255
OAuth2TokenContext accessTokenContext) {
5356

54-
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token.getTokenValue(),
55-
token.getIssuedAt(), token.getExpiresAt(), accessTokenContext.getAuthorizedScopes());
57+
OAuth2AccessToken.TokenType tokenType = OAuth2AccessToken.TokenType.BEARER;
58+
if (token instanceof ClaimAccessor claimAccessor) {
59+
Map<String, Object> cnfClaims = claimAccessor.getClaimAsMap("cnf");
60+
if (!CollectionUtils.isEmpty(cnfClaims) && cnfClaims.containsKey("jkt")) {
61+
tokenType = OAuth2AccessToken.TokenType.DPOP;
62+
}
63+
}
64+
OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, token.getTokenValue(), token.getIssuedAt(),
65+
token.getExpiresAt(), accessTokenContext.getAuthorizedScopes());
5666
OAuth2TokenFormat accessTokenFormat = accessTokenContext.getRegisteredClient()
5767
.getTokenSettings()
5868
.getAccessTokenFormat();

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -185,6 +185,9 @@ public Authentication authenticate(Authentication authentication) throws Authent
185185
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
186186
}
187187

188+
// Verify the DPoP Proof (if available)
189+
Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(authorizationCodeAuthentication);
190+
188191
if (this.logger.isTraceEnabled()) {
189192
this.logger.trace("Validated token request parameters");
190193
}
@@ -201,6 +204,9 @@ public Authentication authenticate(Authentication authentication) throws Authent
201204
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
202205
.authorizationGrant(authorizationCodeAuthentication);
203206
// @formatter:on
207+
if (dPoPProof != null) {
208+
tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
209+
}
204210

205211
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
206212

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@
3232
import org.springframework.security.oauth2.core.OAuth2Error;
3333
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
3434
import org.springframework.security.oauth2.core.OAuth2Token;
35+
import org.springframework.security.oauth2.jwt.Jwt;
3536
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
3637
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
3738
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
@@ -116,21 +117,27 @@ public Authentication authenticate(Authentication authentication) throws Authent
116117

117118
Set<String> authorizedScopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
118119

120+
// Verify the DPoP Proof (if available)
121+
Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(clientCredentialsAuthentication);
122+
119123
if (this.logger.isTraceEnabled()) {
120124
this.logger.trace("Validated token request parameters");
121125
}
122126

123127
// @formatter:off
124-
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
128+
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
125129
.registeredClient(registeredClient)
126130
.principal(clientPrincipal)
127131
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
128132
.authorizedScopes(authorizedScopes)
129133
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
130134
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
131-
.authorizationGrant(clientCredentialsAuthentication)
132-
.build();
135+
.authorizationGrant(clientCredentialsAuthentication);
133136
// @formatter:on
137+
if (dPoPProof != null) {
138+
tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
139+
}
140+
OAuth2TokenContext tokenContext = tokenContextBuilder.build();
134141

135142
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
136143
if (generatedAccessToken == null) {

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@
3434
import org.springframework.security.oauth2.core.OAuth2Token;
3535
import org.springframework.security.oauth2.core.OAuth2UserCode;
3636
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
37+
import org.springframework.security.oauth2.jwt.Jwt;
3738
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
3839
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
3940
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
@@ -182,6 +183,9 @@ public Authentication authenticate(Authentication authentication) throws Authent
182183
throw new OAuth2AuthenticationException(error);
183184
}
184185

186+
// Verify the DPoP Proof (if available)
187+
Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(deviceCodeAuthentication);
188+
185189
if (this.logger.isTraceEnabled()) {
186190
this.logger.trace("Validated device token request parameters");
187191
}
@@ -196,6 +200,9 @@ public Authentication authenticate(Authentication authentication) throws Authent
196200
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
197201
.authorizationGrant(deviceCodeAuthentication);
198202
// @formatter:on
203+
if (dPoPProof != null) {
204+
tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
205+
}
199206

200207
// @formatter:off
201208
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization)

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -157,6 +157,9 @@ public Authentication authenticate(Authentication authentication) throws Authent
157157
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
158158
}
159159

160+
// Verify the DPoP Proof (if available)
161+
Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(refreshTokenAuthentication);
162+
160163
if (this.logger.isTraceEnabled()) {
161164
this.logger.trace("Validated token request parameters");
162165
}
@@ -175,6 +178,9 @@ public Authentication authenticate(Authentication authentication) throws Authent
175178
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
176179
.authorizationGrant(refreshTokenAuthentication);
177180
// @formatter:on
181+
if (dPoPProof != null) {
182+
tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
183+
}
178184

179185
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
180186

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationProvider.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@
3838
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
3939
import org.springframework.security.oauth2.core.OAuth2Token;
4040
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
41+
import org.springframework.security.oauth2.jwt.Jwt;
4142
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
4243
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
4344
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
@@ -203,6 +204,9 @@ else if (!CollectionUtils.isEmpty(subjectAuthorization.getAuthorizedScopes())) {
203204
authorizedScopes = validateRequestedScopes(registeredClient, subjectAuthorization.getAuthorizedScopes());
204205
}
205206

207+
// Verify the DPoP Proof (if available)
208+
Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(tokenExchangeAuthentication);
209+
206210
if (this.logger.isTraceEnabled()) {
207211
this.logger.trace("Validated token request parameters");
208212
}
@@ -220,6 +224,9 @@ else if (!CollectionUtils.isEmpty(subjectAuthorization.getAuthorizedScopes())) {
220224
.authorizationGrantType(AuthorizationGrantType.TOKEN_EXCHANGE)
221225
.authorizationGrant(tokenExchangeAuthentication);
222226
// @formatter:on
227+
if (dPoPProof != null) {
228+
tokenContextBuilder.put(OAuth2TokenContext.DPOP_PROOF_KEY, dPoPProof);
229+
}
223230

224231
// ----- Access token -----
225232
OAuth2TokenContext tokenContext = tokenContextBuilder.build();

0 commit comments

Comments
 (0)