Skip to content

Add Support DPoP Customization #17202

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,53 +16,34 @@

package org.springframework.security.config.annotation.web.configurers.oauth2.server.resource;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationToken;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.DPoPAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.DPoPRequestMatcher;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationFilter;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.Assert;

/**
* An {@link AbstractHttpConfigurer} for OAuth 2.0 Demonstrating Proof of Possession
* (DPoP) support.
*
* @author Joe Grandja
* @author Max Batischev
* @since 6.5
* @see DPoPAuthenticationProvider
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc9449">RFC 9449
* OAuth 2.0 Demonstrating Proof of Possession (DPoP)</a>
*/
final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>>
public final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>>
extends AbstractHttpConfigurer<DPoPAuthenticationConfigurer<B>, B> {

private RequestMatcher requestMatcher;
Expand All @@ -87,6 +68,50 @@ public void configure(B http) {
http.addFilter(authenticationFilter);
}

/**
* Sets the {@link RequestMatcher} to use.
* @param requestMatcher
* @since 7.0
*/
public DPoPAuthenticationConfigurer<B> requestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requestMatcher = requestMatcher;
return this;
}

/**
* Sets the {@link AuthenticationConverter} to use.
* @param authenticationConverter
* @since 7.0
*/
public DPoPAuthenticationConfigurer<B> authenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
return this;
}

/**
* Sets the {@link AuthenticationFailureHandler} to use.
* @param failureHandler
* @since 7.0
*/
public DPoPAuthenticationConfigurer<B> failureHandler(AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.authenticationFailureHandler = failureHandler;
return this;
}

/**
* Sets the {@link AuthenticationSuccessHandler} to use.
* @param successHandler
* @since 7.0
*/
public DPoPAuthenticationConfigurer<B> successHandler(AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler, "successHandler cannot be null");
this.authenticationSuccessHandler = successHandler;
return this;
}

private RequestMatcher getRequestMatcher() {
if (this.requestMatcher == null) {
this.requestMatcher = new DPoPRequestMatcher();
Expand Down Expand Up @@ -118,101 +143,4 @@ private AuthenticationFailureHandler getAuthenticationFailureHandler() {
return this.authenticationFailureHandler;
}

private static final class DPoPRequestMatcher implements RequestMatcher {

@Override
public boolean matches(HttpServletRequest request) {
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
if (!StringUtils.hasText(authorization)) {
return false;
}
return StringUtils.startsWithIgnoreCase(authorization, OAuth2AccessToken.TokenType.DPOP.getValue());
}

}

private static final class DPoPAuthenticationConverter implements AuthenticationConverter {

private static final Pattern AUTHORIZATION_PATTERN = Pattern.compile("^DPoP (?<token>[a-zA-Z0-9-._~+/]+=*)$",
Pattern.CASE_INSENSITIVE);

@Override
public Authentication convert(HttpServletRequest request) {
List<String> authorizationList = Collections.list(request.getHeaders(HttpHeaders.AUTHORIZATION));
if (CollectionUtils.isEmpty(authorizationList)) {
return null;
}
if (authorizationList.size() != 1) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST,
"Found multiple Authorization headers.", null);
throw new OAuth2AuthenticationException(error);
}
String authorization = authorizationList.get(0);
if (!StringUtils.startsWithIgnoreCase(authorization, OAuth2AccessToken.TokenType.DPOP.getValue())) {
return null;
}
Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization);
if (!matcher.matches()) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "DPoP access token is malformed.",
null);
throw new OAuth2AuthenticationException(error);
}
String accessToken = matcher.group("token");
List<String> dPoPProofList = Collections
.list(request.getHeaders(OAuth2AccessToken.TokenType.DPOP.getValue()));
if (CollectionUtils.isEmpty(dPoPProofList) || dPoPProofList.size() != 1) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST,
"DPoP proof is missing or invalid.", null);
throw new OAuth2AuthenticationException(error);
}
String dPoPProof = dPoPProofList.get(0);
return new DPoPAuthenticationToken(accessToken, dPoPProof, request.getMethod(),
request.getRequestURL().toString());
}

}

private static final class DPoPAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authenticationException) {
Map<String, String> parameters = new LinkedHashMap<>();
if (authenticationException instanceof OAuth2AuthenticationException oauth2AuthenticationException) {
OAuth2Error error = oauth2AuthenticationException.getError();
parameters.put(OAuth2ParameterNames.ERROR, error.getErrorCode());
if (StringUtils.hasText(error.getDescription())) {
parameters.put(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
}
if (StringUtils.hasText(error.getUri())) {
parameters.put(OAuth2ParameterNames.ERROR_URI, error.getUri());
}
}
parameters.put("algs",
JwsAlgorithms.RS256 + " " + JwsAlgorithms.RS384 + " " + JwsAlgorithms.RS512 + " "
+ JwsAlgorithms.PS256 + " " + JwsAlgorithms.PS384 + " " + JwsAlgorithms.PS512 + " "
+ JwsAlgorithms.ES256 + " " + JwsAlgorithms.ES384 + " " + JwsAlgorithms.ES512);
String wwwAuthenticate = toWWWAuthenticateHeader(parameters);
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}

private static String toWWWAuthenticateHeader(Map<String, String> parameters) {
StringBuilder wwwAuthenticate = new StringBuilder();
wwwAuthenticate.append(OAuth2AccessToken.TokenType.DPOP.getValue());
if (!parameters.isEmpty()) {
wwwAuthenticate.append(" ");
int i = 0;
for (Map.Entry<String, String> entry : parameters.entrySet()) {
wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\"");
if (i++ != parameters.size() - 1) {
wwwAuthenticate.append(", ");
}
}
}
return wwwAuthenticate.toString();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
* @author Josh Cummings
* @author Evgeniy Cheban
* @author Jerome Wacongne &lt;[email protected]&gt;
* @author Max Batischev
* @since 5.1
* @see BearerTokenAuthenticationFilter
* @see JwtAuthenticationProvider
Expand All @@ -152,7 +153,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<

private final ApplicationContext context;

private final DPoPAuthenticationConfigurer<H> dPoPAuthenticationConfigurer = new DPoPAuthenticationConfigurer<>();
private DPoPAuthenticationConfigurer<H> dPoPAuthenticationConfigurer;

private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;

Expand Down Expand Up @@ -257,6 +258,22 @@ public OAuth2ResourceServerConfigurer<H> opaqueToken(Customizer<OpaqueTokenConfi
return this;
}

/**
* Enables DPoP support.
* @param dpopAuthenticatioCustomizer the {@link Customizer} to provide more options
* for the {@link DPoPAuthenticationConfigurer}
* @return the {@link OAuth2ResourceServerConfigurer} for further customizations
* @since 7.0
*/
public OAuth2ResourceServerConfigurer<H> dpop(
Customizer<DPoPAuthenticationConfigurer<H>> dpopAuthenticatioCustomizer) {
if (this.dPoPAuthenticationConfigurer == null) {
this.dPoPAuthenticationConfigurer = new DPoPAuthenticationConfigurer<>();
}
dpopAuthenticatioCustomizer.customize(this.dPoPAuthenticationConfigurer);
return this;
}

@Override
public void init(H http) {
validateConfiguration();
Expand Down Expand Up @@ -285,7 +302,9 @@ public void configure(H http) {
filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
filter = postProcess(filter);
http.addFilter(filter);
this.dPoPAuthenticationConfigurer.configure(http);
if (this.dPoPAuthenticationConfigurer != null) {
this.dPoPAuthenticationConfigurer.configure(http);
}
}

private void validateConfiguration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
Expand Down Expand Up @@ -125,15 +125,15 @@ public BeanDefinition parse(Element oauth2ResourceServer, ParserContext pc) {
}
BeanMetadataElement bearerTokenResolver = getBearerTokenResolver(oauth2ResourceServer);
BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder
.rootBeanDefinition(BearerTokenRequestMatcher.class);
.rootBeanDefinition(BearerTokenRequestMatcher.class);
requestMatcherBuilder.addConstructorArgValue(bearerTokenResolver);
BeanDefinition requestMatcher = requestMatcherBuilder.getBeanDefinition();
BeanMetadataElement authenticationEntryPoint = getEntryPoint(oauth2ResourceServer);
this.entryPoints.put(requestMatcher, authenticationEntryPoint);
this.deniedHandlers.put(requestMatcher, this.accessDeniedHandler);
this.ignoreCsrfRequestMatchers.add(requestMatcher);
BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder
.rootBeanDefinition(BearerTokenAuthenticationFilter.class);
.rootBeanDefinition(BearerTokenAuthenticationFilter.class);
BeanMetadataElement authenticationManagerResolver = getAuthenticationManagerResolver(oauth2ResourceServer);
filterBuilder.addConstructorArgValue(authenticationManagerResolver);
filterBuilder.addPropertyValue(BEARER_TOKEN_RESOLVER, bearerTokenResolver);
Expand All @@ -147,20 +147,20 @@ void validateConfiguration(Element oauth2ResourceServer, Element jwt, Element op
if (!oauth2ResourceServer.hasAttribute(AUTHENTICATION_MANAGER_RESOLVER_REF)) {
if (jwt == null && opaqueToken == null) {
pc.getReaderContext()
.error("Didn't find authentication-manager-resolver-ref, " + "<jwt>, or <opaque-token>. "
+ "Please select one.", oauth2ResourceServer);
.error("Didn't find authentication-manager-resolver-ref, " + "<jwt>, or <opaque-token>. "
+ "Please select one.", oauth2ResourceServer);
}
return;
}
if (jwt != null) {
pc.getReaderContext()
.error("Found <jwt> as well as authentication-manager-resolver-ref. Please select just one.",
oauth2ResourceServer);
.error("Found <jwt> as well as authentication-manager-resolver-ref. Please select just one.",
oauth2ResourceServer);
}
if (opaqueToken != null) {
pc.getReaderContext()
.error("Found <opaque-token> as well as authentication-manager-resolver-ref. Please select just one.",
oauth2ResourceServer);
.error("Found <opaque-token> as well as authentication-manager-resolver-ref. Please select just one.",
oauth2ResourceServer);
}
}

Expand All @@ -170,7 +170,7 @@ BeanMetadataElement getAuthenticationManagerResolver(Element element) {
return new RuntimeBeanReference(authenticationManagerResolverRef);
}
BeanDefinitionBuilder authenticationManagerResolver = BeanDefinitionBuilder
.rootBeanDefinition(StaticAuthenticationManagerResolver.class);
.rootBeanDefinition(StaticAuthenticationManagerResolver.class);
authenticationManagerResolver.addConstructorArgValue(this.authenticationManager);
return authenticationManagerResolver.getBeanDefinition();
}
Expand Down Expand Up @@ -208,7 +208,7 @@ static final class JwtBeanDefinitionParser implements BeanDefinitionParser {
public BeanDefinition parse(Element element, ParserContext pc) {
validateConfiguration(element, pc);
BeanDefinitionBuilder jwtProviderBuilder = BeanDefinitionBuilder
.rootBeanDefinition(JwtAuthenticationProvider.class);
.rootBeanDefinition(JwtAuthenticationProvider.class);
jwtProviderBuilder.addConstructorArgValue(getDecoder(element));
jwtProviderBuilder.addPropertyValue(JWT_AUTHENTICATION_CONVERTER, getJwtAuthenticationConverter(element));
return jwtProviderBuilder.getBeanDefinition();
Expand All @@ -228,7 +228,7 @@ Object getDecoder(Element element) {
return new RuntimeBeanReference(decoderRef);
}
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.rootBeanDefinition(NimbusJwtDecoderJwkSetUriFactoryBean.class);
.rootBeanDefinition(NimbusJwtDecoderJwkSetUriFactoryBean.class);
builder.addConstructorArgValue(element.getAttribute(JWK_SET_URI));
return builder.getBeanDefinition();
}
Expand Down Expand Up @@ -264,7 +264,7 @@ public BeanDefinition parse(Element element, ParserContext pc) {
BeanMetadataElement introspector = getIntrospector(element);
String authenticationConverterRef = element.getAttribute(AUTHENTICATION_CONVERTER_REF);
BeanDefinitionBuilder opaqueTokenProviderBuilder = BeanDefinitionBuilder
.rootBeanDefinition(OpaqueTokenAuthenticationProvider.class);
.rootBeanDefinition(OpaqueTokenAuthenticationProvider.class);
opaqueTokenProviderBuilder.addConstructorArgValue(introspector);
if (StringUtils.hasText(authenticationConverterRef)) {
opaqueTokenProviderBuilder.addPropertyReference(AUTHENTICATION_CONVERTER, authenticationConverterRef);
Expand All @@ -278,15 +278,15 @@ void validateConfiguration(Element element, ParserContext pc) {
|| element.hasAttribute(CLIENT_SECRET);
if (usesIntrospector == usesEndpoint) {
pc.getReaderContext()
.error("Please specify either introspector-ref or all of "
+ "introspection-uri, client-id, and client-secret.", element);
.error("Please specify either introspector-ref or all of "
+ "introspection-uri, client-id, and client-secret.", element);
return;
}
if (usesEndpoint) {
if (!(element.hasAttribute(INTROSPECTION_URI) && element.hasAttribute(CLIENT_ID)
&& element.hasAttribute(CLIENT_SECRET))) {
pc.getReaderContext()
.error("Please specify introspection-uri, client-id, and client-secret together", element);
.error("Please specify introspection-uri, client-id, and client-secret together", element);
}
}
}
Expand All @@ -300,7 +300,7 @@ BeanMetadataElement getIntrospector(Element element) {
String clientId = element.getAttribute(CLIENT_ID);
String clientSecret = element.getAttribute(CLIENT_SECRET);
BeanDefinitionBuilder introspectorBuilder = BeanDefinitionBuilder
.rootBeanDefinition(SpringOpaqueTokenIntrospector.class);
.rootBeanDefinition(NimbusOpaqueTokenIntrospector.class);
introspectorBuilder.addConstructorArgValue(introspectionUri);
introspectorBuilder.addConstructorArgValue(clientId);
introspectorBuilder.addConstructorArgValue(clientSecret);
Expand Down
Loading
Loading