3 changed files with 246 additions and 50 deletions
@ -0,0 +1,111 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present 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 java.util.Collections; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.function.Consumer; |
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable; |
||||||
|
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt; |
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
/** |
||||||
|
* An {@link OAuth2AuthenticationContext} that holds an |
||||||
|
* {@link OAuth2RefreshTokenAuthenticationToken} and additional information and is used |
||||||
|
* when validating the OAuth 2.0 Refresh Token Grant Request. |
||||||
|
* <p> |
||||||
|
* This context provides access to the current {@link OAuth2Authorization}, |
||||||
|
* {@link OAuth2ClientAuthenticationToken}, and optionally a DPoP {@link Jwt} proof. |
||||||
|
* </p> |
||||||
|
* |
||||||
|
* @author Andrey Litvitski |
||||||
|
* @since 7.0.0 |
||||||
|
* @see OAuth2AuthenticationContext |
||||||
|
* @see OAuth2RefreshTokenAuthenticationProvider#setAuthenticationValidator(Consumer) |
||||||
|
*/ |
||||||
|
public final class OAuth2RefreshTokenAuthenticationContext implements OAuth2AuthenticationContext { |
||||||
|
|
||||||
|
private final Map<Object, Object> context; |
||||||
|
|
||||||
|
private OAuth2RefreshTokenAuthenticationContext(Map<Object, Object> context) { |
||||||
|
this.context = Collections.unmodifiableMap(new HashMap<>(context)); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public <V> V get(Object key) { |
||||||
|
return hasKey(key) ? (V) this.context.get(key) : null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean hasKey(Object key) { |
||||||
|
Assert.notNull(key, "key cannot be null"); |
||||||
|
return this.context.containsKey(key); |
||||||
|
} |
||||||
|
|
||||||
|
public OAuth2Authorization getAuthorization() { |
||||||
|
return get(OAuth2Authorization.class); |
||||||
|
} |
||||||
|
|
||||||
|
public OAuth2ClientAuthenticationToken getClientPrincipal() { |
||||||
|
return get(OAuth2ClientAuthenticationToken.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable public Jwt getDPoPProof() { |
||||||
|
return get(Jwt.class); |
||||||
|
} |
||||||
|
|
||||||
|
public static Builder with(OAuth2RefreshTokenAuthenticationToken authentication) { |
||||||
|
return new Builder(authentication); |
||||||
|
} |
||||||
|
|
||||||
|
public static final class Builder extends AbstractBuilder<OAuth2RefreshTokenAuthenticationContext, Builder> { |
||||||
|
|
||||||
|
private Builder(OAuth2RefreshTokenAuthenticationToken authentication) { |
||||||
|
super(authentication); |
||||||
|
} |
||||||
|
|
||||||
|
public Builder authorization(OAuth2Authorization authorization) { |
||||||
|
return put(OAuth2Authorization.class, authorization); |
||||||
|
} |
||||||
|
|
||||||
|
public Builder clientPrincipal(OAuth2ClientAuthenticationToken clientPrincipal) { |
||||||
|
return put(OAuth2ClientAuthenticationToken.class, clientPrincipal); |
||||||
|
} |
||||||
|
|
||||||
|
public Builder dPoPProof(@Nullable Jwt dPoPProof) { |
||||||
|
if (dPoPProof != null) { |
||||||
|
put(Jwt.class, dPoPProof); |
||||||
|
} |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public OAuth2RefreshTokenAuthenticationContext build() { |
||||||
|
Assert.notNull(get(OAuth2Authorization.class), "authorization cannot be null"); |
||||||
|
Assert.notNull(get(OAuth2ClientAuthenticationToken.class), "clientPrincipal cannot be null"); |
||||||
|
return new OAuth2RefreshTokenAuthenticationContext(getContext()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,114 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2004-present 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 java.util.Map; |
||||||
|
import java.util.function.Consumer; |
||||||
|
|
||||||
|
import com.nimbusds.jose.jwk.JWK; |
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.ClaimAccessor; |
||||||
|
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.util.CollectionUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* A {@code Consumer} that validates an {@link OAuth2RefreshTokenAuthenticationContext} |
||||||
|
* and acts as the default |
||||||
|
* {@link OAuth2RefreshTokenAuthenticationProvider#setAuthenticationValidator(Consumer) |
||||||
|
* authentication validator} for the Refresh Token grant. |
||||||
|
* <p> |
||||||
|
* The default implementation validates a DPoP proof if present and throws |
||||||
|
* {@link OAuth2AuthenticationException} on failure. |
||||||
|
* </p> |
||||||
|
* |
||||||
|
* @author Andrey Litvitski |
||||||
|
* @since 7.0.0 |
||||||
|
* @see OAuth2RefreshTokenAuthenticationContext |
||||||
|
* @see OAuth2RefreshTokenAuthenticationProvider#setAuthenticationValidator(Consumer) |
||||||
|
*/ |
||||||
|
public final class OAuth2RefreshTokenAuthenticationValidator |
||||||
|
implements Consumer<OAuth2RefreshTokenAuthenticationContext> { |
||||||
|
|
||||||
|
public static final Consumer<OAuth2RefreshTokenAuthenticationContext> DEFAULT_VALIDATOR = OAuth2RefreshTokenAuthenticationValidator::validateDefault; |
||||||
|
|
||||||
|
private final Consumer<OAuth2RefreshTokenAuthenticationContext> authenticationValidator = DEFAULT_VALIDATOR; |
||||||
|
|
||||||
|
@Override |
||||||
|
public void accept(OAuth2RefreshTokenAuthenticationContext context) { |
||||||
|
this.authenticationValidator.accept(context); |
||||||
|
} |
||||||
|
|
||||||
|
private static void validateDefault(OAuth2RefreshTokenAuthenticationContext context) { |
||||||
|
Jwt dPoPProof; |
||||||
|
if (context.getDPoPProof() == null) { |
||||||
|
dPoPProof = DPoPProofVerifier.verifyIfAvailable(context.getAuthentication()); |
||||||
|
} |
||||||
|
else { |
||||||
|
dPoPProof = context.getDPoPProof(); |
||||||
|
} |
||||||
|
if (dPoPProof == null || !context.getClientPrincipal() |
||||||
|
.getClientAuthenticationMethod() |
||||||
|
.equals(ClientAuthenticationMethod.NONE)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
JWK jwk = null; |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
Map<String, Object> jwkJson = (Map<String, Object>) dPoPProof.getHeaders().get("jwk"); |
||||||
|
try { |
||||||
|
jwk = JWK.parse(jwkJson); |
||||||
|
} |
||||||
|
catch (Exception ignored) { |
||||||
|
} |
||||||
|
if (jwk == null) { |
||||||
|
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF, |
||||||
|
"jwk header is missing or invalid.", null); |
||||||
|
throw new OAuth2AuthenticationException(error); |
||||||
|
} |
||||||
|
|
||||||
|
String jwkThumbprint; |
||||||
|
try { |
||||||
|
jwkThumbprint = jwk.computeThumbprint().toString(); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF, |
||||||
|
"Failed to compute SHA-256 Thumbprint for jwk.", null); |
||||||
|
throw new OAuth2AuthenticationException(error); |
||||||
|
} |
||||||
|
|
||||||
|
String jwkThumbprintClaim = null; |
||||||
|
Map<String, Object> accessTokenClaimsMap = context.getAuthorization().getAccessToken().getClaims(); |
||||||
|
ClaimAccessor accessTokenClaims = () -> accessTokenClaimsMap; |
||||||
|
Map<String, Object> confirmationMethodClaim = accessTokenClaims.getClaimAsMap("cnf"); |
||||||
|
if (!CollectionUtils.isEmpty(confirmationMethodClaim) && confirmationMethodClaim.containsKey("jkt")) { |
||||||
|
jwkThumbprintClaim = (String) confirmationMethodClaim.get("jkt"); |
||||||
|
} |
||||||
|
if (jwkThumbprintClaim == null) { |
||||||
|
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF, "jkt claim is missing.", null); |
||||||
|
throw new OAuth2AuthenticationException(error); |
||||||
|
} |
||||||
|
|
||||||
|
if (!jwkThumbprint.equals(jwkThumbprintClaim)) { |
||||||
|
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF, "jwk header is invalid.", null); |
||||||
|
throw new OAuth2AuthenticationException(error); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue