Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit dcc85d8

Browse files
Add generic request validator for refresh token
Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
1 parent 727f0e2 commit dcc85d8

File tree

3 files changed

+246
-50
lines changed

3 files changed

+246
-50
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2004-present 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+
17+
package org.springframework.security.oauth2.server.authorization.authentication;
18+
19+
import java.util.Collections;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
import java.util.function.Consumer;
23+
24+
import org.jspecify.annotations.Nullable;
25+
26+
import org.springframework.security.oauth2.jwt.Jwt;
27+
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* An {@link OAuth2AuthenticationContext} that holds an
32+
* {@link OAuth2RefreshTokenAuthenticationToken} and additional information and is used
33+
* when validating the OAuth 2.0 Refresh Token Grant Request.
34+
* <p>
35+
* This context provides access to the current {@link OAuth2Authorization},
36+
* {@link OAuth2ClientAuthenticationToken}, and optionally a DPoP {@link Jwt} proof.
37+
* </p>
38+
*
39+
* @author Andrey Litvitski
40+
* @since 7.0.0
41+
* @see OAuth2AuthenticationContext
42+
* @see OAuth2RefreshTokenAuthenticationProvider#setAuthenticationValidator(Consumer)
43+
*/
44+
public final class OAuth2RefreshTokenAuthenticationContext implements OAuth2AuthenticationContext {
45+
46+
private final Map<Object, Object> context;
47+
48+
private OAuth2RefreshTokenAuthenticationContext(Map<Object, Object> context) {
49+
this.context = Collections.unmodifiableMap(new HashMap<>(context));
50+
}
51+
52+
@SuppressWarnings("unchecked")
53+
@Nullable
54+
@Override
55+
public <V> V get(Object key) {
56+
return hasKey(key) ? (V) this.context.get(key) : null;
57+
}
58+
59+
@Override
60+
public boolean hasKey(Object key) {
61+
Assert.notNull(key, "key cannot be null");
62+
return this.context.containsKey(key);
63+
}
64+
65+
public OAuth2Authorization getAuthorization() {
66+
return get(OAuth2Authorization.class);
67+
}
68+
69+
public OAuth2ClientAuthenticationToken getClientPrincipal() {
70+
return get(OAuth2ClientAuthenticationToken.class);
71+
}
72+
73+
@Nullable public Jwt getDPoPProof() {
74+
return get(Jwt.class);
75+
}
76+
77+
public static Builder with(OAuth2RefreshTokenAuthenticationToken authentication) {
78+
return new Builder(authentication);
79+
}
80+
81+
public static final class Builder extends AbstractBuilder<OAuth2RefreshTokenAuthenticationContext, Builder> {
82+
83+
private Builder(OAuth2RefreshTokenAuthenticationToken authentication) {
84+
super(authentication);
85+
}
86+
87+
public Builder authorization(OAuth2Authorization authorization) {
88+
return put(OAuth2Authorization.class, authorization);
89+
}
90+
91+
public Builder clientPrincipal(OAuth2ClientAuthenticationToken clientPrincipal) {
92+
return put(OAuth2ClientAuthenticationToken.class, clientPrincipal);
93+
}
94+
95+
public Builder dPoPProof(@Nullable Jwt dPoPProof) {
96+
if (dPoPProof != null) {
97+
put(Jwt.class, dPoPProof);
98+
}
99+
return this;
100+
}
101+
102+
@Override
103+
public OAuth2RefreshTokenAuthenticationContext build() {
104+
Assert.notNull(get(OAuth2Authorization.class), "authorization cannot be null");
105+
Assert.notNull(get(OAuth2ClientAuthenticationToken.class), "clientPrincipal cannot be null");
106+
return new OAuth2RefreshTokenAuthenticationContext(getContext());
107+
}
108+
109+
}
110+
111+
}

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

Lines changed: 21 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
import java.util.HashMap;
2222
import java.util.Map;
2323
import java.util.Set;
24+
import java.util.function.Consumer;
2425

25-
import com.nimbusds.jose.jwk.JWK;
2626
import org.apache.commons.logging.Log;
2727
import org.apache.commons.logging.LogFactory;
2828

@@ -31,8 +31,6 @@
3131
import org.springframework.security.core.Authentication;
3232
import org.springframework.security.core.AuthenticationException;
3333
import org.springframework.security.oauth2.core.AuthorizationGrantType;
34-
import org.springframework.security.oauth2.core.ClaimAccessor;
35-
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
3634
import org.springframework.security.oauth2.core.OAuth2AccessToken;
3735
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
3836
import org.springframework.security.oauth2.core.OAuth2Error;
@@ -52,14 +50,14 @@
5250
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
5351
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
5452
import org.springframework.util.Assert;
55-
import org.springframework.util.CollectionUtils;
5653

5754
/**
5855
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Refresh Token Grant.
5956
*
6057
* @author Alexey Nesterov
6158
* @author Joe Grandja
6259
* @author Anoop Garlapati
60+
* @author Andrey Litvitski
6361
* @since 7.0
6462
* @see OAuth2RefreshTokenAuthenticationToken
6563
* @see OAuth2AccessTokenAuthenticationToken
@@ -84,6 +82,8 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
8482

8583
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
8684

85+
private Consumer<OAuth2RefreshTokenAuthenticationContext> authenticationValidator = new OAuth2RefreshTokenAuthenticationValidator();
86+
8787
/**
8888
* Constructs an {@code OAuth2RefreshTokenAuthenticationProvider} using the provided
8989
* parameters.
@@ -164,13 +164,14 @@ public Authentication authenticate(Authentication authentication) throws Authent
164164
// Verify the DPoP Proof (if available)
165165
Jwt dPoPProof = DPoPProofVerifier.verifyIfAvailable(refreshTokenAuthentication);
166166

167-
if (dPoPProof != null
168-
&& clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
169-
// For public clients, verify the DPoP Proof public key is same as (current)
170-
// access token public key binding
171-
Map<String, Object> accessTokenClaims = authorization.getAccessToken().getClaims();
172-
verifyDPoPProofPublicKey(dPoPProof, () -> accessTokenClaims);
173-
}
167+
OAuth2RefreshTokenAuthenticationContext context = OAuth2RefreshTokenAuthenticationContext
168+
.with(refreshTokenAuthentication)
169+
.authorization(authorization)
170+
.clientPrincipal(clientPrincipal)
171+
.dPoPProof(dPoPProof)
172+
.build();
173+
174+
this.authenticationValidator.accept(context);
174175

175176
if (this.logger.isTraceEnabled()) {
176177
this.logger.trace("Validated token request parameters");
@@ -292,45 +293,15 @@ public boolean supports(Class<?> authentication) {
292293
return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);
293294
}
294295

295-
private static void verifyDPoPProofPublicKey(Jwt dPoPProof, ClaimAccessor accessTokenClaims) {
296-
JWK jwk = null;
297-
@SuppressWarnings("unchecked")
298-
Map<String, Object> jwkJson = (Map<String, Object>) dPoPProof.getHeaders().get("jwk");
299-
try {
300-
jwk = JWK.parse(jwkJson);
301-
}
302-
catch (Exception ignored) {
303-
}
304-
if (jwk == null) {
305-
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
306-
"jwk header is missing or invalid.", null);
307-
throw new OAuth2AuthenticationException(error);
308-
}
309-
310-
String jwkThumbprint;
311-
try {
312-
jwkThumbprint = jwk.computeThumbprint().toString();
313-
}
314-
catch (Exception ex) {
315-
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
316-
"Failed to compute SHA-256 Thumbprint for jwk.", null);
317-
throw new OAuth2AuthenticationException(error);
318-
}
319-
320-
String jwkThumbprintClaim = null;
321-
Map<String, Object> confirmationMethodClaim = accessTokenClaims.getClaimAsMap("cnf");
322-
if (!CollectionUtils.isEmpty(confirmationMethodClaim) && confirmationMethodClaim.containsKey("jkt")) {
323-
jwkThumbprintClaim = (String) confirmationMethodClaim.get("jkt");
324-
}
325-
if (jwkThumbprintClaim == null) {
326-
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF, "jkt claim is missing.", null);
327-
throw new OAuth2AuthenticationException(error);
328-
}
329-
330-
if (!jwkThumbprint.equals(jwkThumbprintClaim)) {
331-
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF, "jwk header is invalid.", null);
332-
throw new OAuth2AuthenticationException(error);
333-
}
296+
/**
297+
* Sets the {@code Consumer} responsible for validating the OAuth 2.0 Refresh Token
298+
* Grant Request using the provided {@link OAuth2RefreshTokenAuthenticationContext}.
299+
* <p>
300+
* The default validator performs DPoP proof verification if present.
301+
*/
302+
public void setAuthenticationValidator(Consumer<OAuth2RefreshTokenAuthenticationContext> authenticationValidator) {
303+
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
304+
this.authenticationValidator = authenticationValidator;
334305
}
335306

336307
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2004-present 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+
17+
package org.springframework.security.oauth2.server.authorization.authentication;
18+
19+
import java.util.Map;
20+
import java.util.function.Consumer;
21+
22+
import com.nimbusds.jose.jwk.JWK;
23+
24+
import org.springframework.security.oauth2.core.ClaimAccessor;
25+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
26+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
27+
import org.springframework.security.oauth2.core.OAuth2Error;
28+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
29+
import org.springframework.security.oauth2.jwt.Jwt;
30+
import org.springframework.util.CollectionUtils;
31+
32+
/**
33+
* A {@code Consumer} that validates an {@link OAuth2RefreshTokenAuthenticationContext}
34+
* and acts as the default
35+
* {@link OAuth2RefreshTokenAuthenticationProvider#setAuthenticationValidator(Consumer)
36+
* authentication validator} for the Refresh Token grant.
37+
* <p>
38+
* The default implementation validates a DPoP proof if present and throws
39+
* {@link OAuth2AuthenticationException} on failure.
40+
* </p>
41+
*
42+
* @author Andrey Litvitski
43+
* @since 7.0.0
44+
* @see OAuth2RefreshTokenAuthenticationContext
45+
* @see OAuth2RefreshTokenAuthenticationProvider#setAuthenticationValidator(Consumer)
46+
*/
47+
public final class OAuth2RefreshTokenAuthenticationValidator
48+
implements Consumer<OAuth2RefreshTokenAuthenticationContext> {
49+
50+
public static final Consumer<OAuth2RefreshTokenAuthenticationContext> DEFAULT_VALIDATOR = OAuth2RefreshTokenAuthenticationValidator::validateDefault;
51+
52+
private final Consumer<OAuth2RefreshTokenAuthenticationContext> authenticationValidator = DEFAULT_VALIDATOR;
53+
54+
@Override
55+
public void accept(OAuth2RefreshTokenAuthenticationContext context) {
56+
this.authenticationValidator.accept(context);
57+
}
58+
59+
private static void validateDefault(OAuth2RefreshTokenAuthenticationContext context) {
60+
Jwt dPoPProof;
61+
if (context.getDPoPProof() == null) {
62+
dPoPProof = DPoPProofVerifier.verifyIfAvailable(context.getAuthentication());
63+
}
64+
else {
65+
dPoPProof = context.getDPoPProof();
66+
}
67+
if (dPoPProof == null || !context.getClientPrincipal()
68+
.getClientAuthenticationMethod()
69+
.equals(ClientAuthenticationMethod.NONE)) {
70+
return;
71+
}
72+
JWK jwk = null;
73+
@SuppressWarnings("unchecked")
74+
Map<String, Object> jwkJson = (Map<String, Object>) dPoPProof.getHeaders().get("jwk");
75+
try {
76+
jwk = JWK.parse(jwkJson);
77+
}
78+
catch (Exception ignored) {
79+
}
80+
if (jwk == null) {
81+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
82+
"jwk header is missing or invalid.", null);
83+
throw new OAuth2AuthenticationException(error);
84+
}
85+
86+
String jwkThumbprint;
87+
try {
88+
jwkThumbprint = jwk.computeThumbprint().toString();
89+
}
90+
catch (Exception ex) {
91+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
92+
"Failed to compute SHA-256 Thumbprint for jwk.", null);
93+
throw new OAuth2AuthenticationException(error);
94+
}
95+
96+
String jwkThumbprintClaim = null;
97+
Map<String, Object> accessTokenClaimsMap = context.getAuthorization().getAccessToken().getClaims();
98+
ClaimAccessor accessTokenClaims = () -> accessTokenClaimsMap;
99+
Map<String, Object> confirmationMethodClaim = accessTokenClaims.getClaimAsMap("cnf");
100+
if (!CollectionUtils.isEmpty(confirmationMethodClaim) && confirmationMethodClaim.containsKey("jkt")) {
101+
jwkThumbprintClaim = (String) confirmationMethodClaim.get("jkt");
102+
}
103+
if (jwkThumbprintClaim == null) {
104+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF, "jkt claim is missing.", null);
105+
throw new OAuth2AuthenticationException(error);
106+
}
107+
108+
if (!jwkThumbprint.equals(jwkThumbprintClaim)) {
109+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF, "jwk header is invalid.", null);
110+
throw new OAuth2AuthenticationException(error);
111+
}
112+
}
113+
114+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /