Add documentation for DPoP support

Closes gh-17072
This commit is contained in:
Joe Grandja
2025-05-07 14:09:23 -04:00
parent 3110f3679a
commit e3c39f02bc
8 changed files with 345 additions and 2 deletions

View File

@@ -23,6 +23,9 @@ import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.util.Assert;
/**
* A context class that holds a DPoP Proof {@link Jwt} and additional parameters
* associated to an Access Token request or a Protected Resource request.
*
* @author Joe Grandja
* @since 6.5
* @see DPoPProofJwtDecoderFactory
@@ -44,28 +47,58 @@ public final class DPoPProofContext {
this.accessToken = accessToken;
}
/**
* Returns the DPoP Proof {@link Jwt}.
* @return the DPoP Proof {@link Jwt}
*/
public String getDPoPProof() {
return this.dPoPProof;
}
/**
* Returns the value of the HTTP method of the request to which the DPoP Proof
* {@link Jwt} is attached.
* @return the value of the HTTP method of the request to which the DPoP Proof
* {@link Jwt} is attached
*/
public String getMethod() {
return this.method;
}
/**
* Returns the value of the HTTP target URI of the request to which the DPoP Proof
* {@link Jwt} is attached, without query and fragment parts.
* @return the value of the HTTP target URI of the request to which the DPoP Proof
* {@link Jwt} is attached
*/
public String getTargetUri() {
return this.targetUri;
}
/**
* Returns the access token if the request is a Protected Resource request.
* @param <T> the type of the access token
* @return the access token if the request is a Protected Resource request or
* {@code null}
*/
@SuppressWarnings("unchecked")
@Nullable
public <T extends OAuth2Token> T getAccessToken() {
return (T) this.accessToken;
}
/**
* Returns a new {@link Builder}, initialized with the DPoP Proof {@link Jwt}.
* @param dPoPProof the DPoP Proof {@link Jwt}
* @return the {@link Builder}
*/
public static Builder withDPoPProof(String dPoPProof) {
return new Builder(dPoPProof);
}
/**
* A builder for {@link DPoPProofContext}.
*/
public static final class Builder {
private String dPoPProof;
@@ -81,21 +114,45 @@ public final class DPoPProofContext {
this.dPoPProof = dPoPProof;
}
/**
* Sets the value of the HTTP method of the request to which the DPoP Proof
* {@link Jwt} is attached.
* @param method the value of the HTTP method of the request to which the DPoP
* Proof {@link Jwt} is attached
* @return the {@link Builder}
*/
public Builder method(String method) {
this.method = method;
return this;
}
/**
* Sets the value of the HTTP target URI of the request to which the DPoP Proof
* {@link Jwt} is attached, without query and fragment parts.
* @param targetUri the value of the HTTP target URI of the request to which the
* DPoP Proof {@link Jwt} is attached
* @return the {@link Builder}
*/
public Builder targetUri(String targetUri) {
this.targetUri = targetUri;
return this;
}
/**
* Sets the access token if the request is a Protected Resource request.
* @param accessToken the access token if the request is a Protected Resource
* request
* @return the {@link Builder}
*/
public Builder accessToken(OAuth2Token accessToken) {
this.accessToken = accessToken;
return this;
}
/**
* Builds a new {@link DPoPProofContext}.
* @return a {@link DPoPProofContext}
*/
public DPoPProofContext build() {
validate();
return new DPoPProofContext(this.dPoPProof, this.method, this.targetUri, this.accessToken);

View File

@@ -48,17 +48,29 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A {@link JwtDecoderFactory factory} that provides a {@link JwtDecoder} for the
* specified {@link DPoPProofContext} and is used for authenticating a DPoP Proof
* {@link Jwt}.
*
* @author Joe Grandja
* @since 6.5
* @see JwtDecoderFactory
* @see DPoPProofContext
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc9449">RFC 9449
* OAuth 2.0 Demonstrating Proof of Possession (DPoP)</a>
*/
public final class DPoPProofJwtDecoderFactory implements JwtDecoderFactory<DPoPProofContext> {
/**
* The default {@code OAuth2TokenValidator<Jwt>} factory that validates the
* {@code htm}, {@code htu}, {@code jti} and {@code iat} claims of the DPoP Proof
* {@link Jwt}.
*/
public static final Function<DPoPProofContext, OAuth2TokenValidator<Jwt>> DEFAULT_JWT_VALIDATOR_FACTORY = defaultJwtValidatorFactory();
private static final JOSEObjectTypeVerifier<SecurityContext> DPOP_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>(
new JOSEObjectType("dpop+jwt"));
public static final Function<DPoPProofContext, OAuth2TokenValidator<Jwt>> DEFAULT_JWT_VALIDATOR_FACTORY = defaultJwtValidatorFactory();
private Function<DPoPProofContext, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = DEFAULT_JWT_VALIDATOR_FACTORY;
@Override
@@ -69,6 +81,14 @@ public final class DPoPProofJwtDecoderFactory implements JwtDecoderFactory<DPoPP
return jwtDecoder;
}
/**
* Sets the factory that provides an {@link OAuth2TokenValidator} for the specified
* {@link DPoPProofContext} and is used by the {@link JwtDecoder}. The default
* {@code OAuth2TokenValidator<Jwt>} factory is
* {@link #DEFAULT_JWT_VALIDATOR_FACTORY}.
* @param jwtValidatorFactory the factory that provides an
* {@link OAuth2TokenValidator} for the specified {@link DPoPProofContext}
*/
public void setJwtValidatorFactory(Function<DPoPProofContext, OAuth2TokenValidator<Jwt>> jwtValidatorFactory) {
Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null");
this.jwtValidatorFactory = jwtValidatorFactory;

View File

@@ -50,10 +50,15 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* An {@link AuthenticationProvider} implementation that is responsible for authenticating
* a DPoP-bound access token for a protected resource request.
*
* @author Joe Grandja
* @since 6.5
* @see DPoPAuthenticationToken
* @see DPoPProofJwtDecoderFactory
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc9449">RFC 9449
* OAuth 2.0 Demonstrating Proof of Possession (DPoP)</a>
*/
public final class DPoPAuthenticationProvider implements AuthenticationProvider {
@@ -61,6 +66,11 @@ public final class DPoPAuthenticationProvider implements AuthenticationProvider
private JwtDecoderFactory<DPoPProofContext> dPoPProofVerifierFactory;
/**
* Constructs a {@code DPoPAuthenticationProvider} using the provided parameters.
* @param tokenAuthenticationManager the {@link AuthenticationManager} used to
* authenticate the DPoP-bound access token
*/
public DPoPAuthenticationProvider(AuthenticationManager tokenAuthenticationManager) {
Assert.notNull(tokenAuthenticationManager, "tokenAuthenticationManager cannot be null");
this.tokenAuthenticationManager = tokenAuthenticationManager;
@@ -121,6 +131,13 @@ public final class DPoPAuthenticationProvider implements AuthenticationProvider
return DPoPAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* Sets the {@link JwtDecoderFactory} that provides a {@link JwtDecoder} for the
* specified {@link DPoPProofContext} and is used for authenticating a DPoP Proof
* {@link Jwt}. The default factory is {@link DPoPProofJwtDecoderFactory}.
* @param dPoPProofVerifierFactory the {@link JwtDecoderFactory} that provides a
* {@link JwtDecoder} for the specified {@link DPoPProofContext}
*/
public void setDPoPProofVerifierFactory(JwtDecoderFactory<DPoPProofContext> dPoPProofVerifierFactory) {
Assert.notNull(dPoPProofVerifierFactory, "dPoPProofVerifierFactory cannot be null");
this.dPoPProofVerifierFactory = dPoPProofVerifierFactory;

View File

@@ -20,9 +20,14 @@ import java.io.Serial;
import java.util.Collections;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
/**
* An {@link Authentication} representing a protected resource request with a DPoP-bound
* access token.
*
* @author Joe Grandja
* @since 6.5
* @see DPoPAuthenticationProvider
@@ -40,6 +45,14 @@ public class DPoPAuthenticationToken extends AbstractAuthenticationToken {
private final String resourceUri;
/**
* Constructs a {@code DPoPAuthenticationToken} using the provided parameters.
* @param accessToken the DPoP-bound access token
* @param dPoPProof the DPoP Proof {@link Jwt}
* @param method the value of the HTTP method of the request
* @param resourceUri the value of the HTTP resource URI of the request, without query
* and fragment parts
*/
public DPoPAuthenticationToken(String accessToken, String dPoPProof, String method, String resourceUri) {
super(Collections.emptyList());
Assert.hasText(accessToken, "accessToken cannot be empty");
@@ -62,18 +75,35 @@ public class DPoPAuthenticationToken extends AbstractAuthenticationToken {
return getAccessToken();
}
/**
* Returns the DPoP-bound access token.
* @return the DPoP-bound access token
*/
public String getAccessToken() {
return this.accessToken;
}
/**
* Returns the DPoP Proof {@link Jwt}.
* @return the DPoP Proof {@link Jwt}
*/
public String getDPoPProof() {
return this.dPoPProof;
}
/**
* Returns the value of the HTTP method of the request.
* @return the value of the HTTP method of the request
*/
public String getMethod() {
return this.method;
}
/**
* Returns the value of the HTTP resource URI of the request, without query and
* fragment parts.
* @return the value of the HTTP resource URI of the request
*/
public String getResourceUri() {
return this.resourceUri;
}