mirror of
https://github.com/spring-projects/spring-security.git
synced 2026-04-21 16:30:27 +00:00
Add Support DPoP Customization
Closes gh-16940 Signed-off-by: Max Batischev <mblancer@mail.ru>
This commit is contained in:
parent
d6b97c7919
commit
fc6a4c8220
@ -16,33 +16,16 @@
|
||||
|
||||
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.authentication.AuthenticationManagerResolver;
|
||||
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;
|
||||
@ -50,8 +33,7 @@ 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;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
@ -61,12 +43,13 @@ import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
* (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;
|
||||
@ -108,6 +91,50 @@ final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
@ -139,101 +166,4 @@ final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -146,6 +146,7 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy;
|
||||
* @author Josh Cummings
|
||||
* @author Evgeniy Cheban
|
||||
* @author Jerome Wacongne <ch4mp@c4-soft.com>
|
||||
* @author Max Batischev
|
||||
* @since 5.1
|
||||
* @see BearerTokenAuthenticationFilter
|
||||
* @see JwtAuthenticationProvider
|
||||
@ -168,6 +169,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
|
||||
private final ApplicationContext context;
|
||||
|
||||
private DPoPAuthenticationConfigurer<H> dPoPAuthenticationConfigurer;
|
||||
|
||||
private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
|
||||
|
||||
private AuthenticationConverter authenticationConverter;
|
||||
@ -268,6 +271,22 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
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();
|
||||
@ -296,9 +315,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
filter = postProcess(filter);
|
||||
http.addFilter(filter);
|
||||
|
||||
if (dPoPAuthenticationAvailable) {
|
||||
DPoPAuthenticationConfigurer<H> dPoPAuthenticationConfigurer = new DPoPAuthenticationConfigurer<>();
|
||||
dPoPAuthenticationConfigurer.configure(http);
|
||||
if (dPoPAuthenticationAvailable && this.dPoPAuthenticationConfigurer != null) {
|
||||
this.dPoPAuthenticationConfigurer.configure(http);
|
||||
}
|
||||
|
||||
OAuth2ProtectedResourceMetadataFilter protectedResourceMetadataFilter = new OAuth2ProtectedResourceMetadataFilter();
|
||||
|
||||
@ -25,12 +25,15 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenResolv
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
import org.springframework.security.web.access.AccessDeniedHandler
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.DPoPAuthenticationConfigurer
|
||||
import org.springframework.security.config.annotation.web.oauth2.resourceserver.DPoPDsl
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure [HttpSecurity] OAuth 2.0 resource server support using
|
||||
* idiomatic Kotlin code.
|
||||
*
|
||||
* @author Eleftheria Stein
|
||||
* @author Max Batischev
|
||||
* @since 5.3
|
||||
* @property accessDeniedHandler the [AccessDeniedHandler] to use for requests authenticating
|
||||
* with <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>s.
|
||||
@ -48,6 +51,7 @@ class OAuth2ResourceServerDsl {
|
||||
|
||||
private var jwt: ((OAuth2ResourceServerConfigurer<HttpSecurity>.JwtConfigurer) -> Unit)? = null
|
||||
private var opaqueToken: ((OAuth2ResourceServerConfigurer<HttpSecurity>.OpaqueTokenConfigurer) -> Unit)? = null
|
||||
private var dpop: ((DPoPAuthenticationConfigurer<HttpSecurity>) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Enables JWT-encoded bearer token support.
|
||||
@ -109,6 +113,36 @@ class OAuth2ResourceServerDsl {
|
||||
this.opaqueToken = OpaqueTokenDsl().apply(opaqueTokenConfig).get()
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables DPoP support.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* @Configuration
|
||||
* @EnableWebSecurity
|
||||
* class SecurityConfig {
|
||||
*
|
||||
* @Bean
|
||||
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
* http {
|
||||
* oauth2ResourceServer {
|
||||
* dpop { }
|
||||
* }
|
||||
* }
|
||||
* return http.build()
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param dpopConfig custom configurations to configure DPoP support
|
||||
* @see [DPoPDsl]
|
||||
* @since 7.0
|
||||
*/
|
||||
fun dpop(dpopConfig: DPoPDsl.() -> Unit) {
|
||||
this.dpop = DPoPDsl().apply(dpopConfig).get()
|
||||
}
|
||||
|
||||
internal fun get(): (OAuth2ResourceServerConfigurer<HttpSecurity>) -> Unit {
|
||||
return { oauth2ResourceServer ->
|
||||
accessDeniedHandler?.also { oauth2ResourceServer.accessDeniedHandler(accessDeniedHandler) }
|
||||
@ -117,6 +151,7 @@ class OAuth2ResourceServerDsl {
|
||||
authenticationManagerResolver?.also { oauth2ResourceServer.authenticationManagerResolver(authenticationManagerResolver) }
|
||||
jwt?.also { oauth2ResourceServer.jwt(jwt) }
|
||||
opaqueToken?.also { oauth2ResourceServer.opaqueToken(opaqueToken) }
|
||||
dpop?.also { oauth2ResourceServer.dpop(dpop) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.config.annotation.web.oauth2.resourceserver
|
||||
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.DPoPAuthenticationConfigurer
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure DPoP support using idiomatic Kotlin code.
|
||||
*
|
||||
* @author Max Batischev
|
||||
* @property requestMatcher the [RequestMatcher] to use.
|
||||
* @property authenticationConverter the [AuthenticationConverter] to use.
|
||||
* @property successHandler the [AuthenticationSuccessHandler] to use.
|
||||
* @property failureHandler the [AuthenticationFailureHandler] to use.
|
||||
* @since 7.0
|
||||
*/
|
||||
class DPoPDsl {
|
||||
var requestMatcher: RequestMatcher? = null
|
||||
var authenticationConverter: AuthenticationConverter? = null
|
||||
var successHandler: AuthenticationSuccessHandler? = null
|
||||
var failureHandler: AuthenticationFailureHandler? = null
|
||||
|
||||
internal fun get(): (DPoPAuthenticationConfigurer<HttpSecurity>) -> Unit {
|
||||
return { dpop ->
|
||||
requestMatcher?.also { dpop.requestMatcher(requestMatcher) }
|
||||
authenticationConverter?.also { dpop.authenticationConverter(authenticationConverter) }
|
||||
successHandler?.also { dpop.successHandler(successHandler) }
|
||||
failureHandler?.also { dpop.failureHandler(failureHandler) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -686,6 +686,19 @@ opaque-token.attlist &=
|
||||
## Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful introspection result into an Authentication.
|
||||
attribute authentication-converter-ref {xsd:token}?
|
||||
|
||||
dpop =
|
||||
## Configuration DpoP
|
||||
element dpop {dpop.attlist}
|
||||
dpop.attlist &=
|
||||
## DPoP Request Matcher
|
||||
attribute dpop-request-matcher-ref {xsd:token}?
|
||||
dpop.attlist &=
|
||||
attribute dpop-authentication-converter-ref {xsd:token}?
|
||||
dpop.attlist &=
|
||||
attribute dpop-success-handler-ref {xsd:token}?
|
||||
dpop.attlist &=
|
||||
attribute dpop-failure-handler-ref {xsd:token}?
|
||||
|
||||
saml2-login =
|
||||
## Configures authentication support for SAML 2.0 Login
|
||||
element saml2-login {saml2-login.attlist}
|
||||
|
||||
@ -2074,6 +2074,26 @@
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:attributeGroup>
|
||||
<xs:element name="dpop">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Configuration DpoP
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:complexType>
|
||||
<xs:attributeGroup ref="security:dpop.attlist"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:attributeGroup name="dpop.attlist">
|
||||
<xs:attribute name="dpop-request-matcher-ref" type="xs:token">
|
||||
<xs:annotation>
|
||||
<xs:documentation>DPoP Request Matcher
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="dpop-authentication-converter-ref" type="xs:token"/>
|
||||
<xs:attribute name="dpop-success-handler-ref" type="xs:token"/>
|
||||
<xs:attribute name="dpop-failure-handler-ref" type="xs:token"/>
|
||||
</xs:attributeGroup>
|
||||
|
||||
<xs:attributeGroup name="saml2-login.attlist">
|
||||
<xs:attribute name="relying-party-registration-repository-ref" type="xs:token">
|
||||
|
||||
@ -37,6 +37,8 @@ import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@ -51,6 +53,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.TestKeys;
|
||||
@ -62,12 +65,16 @@ import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
@ -176,6 +183,22 @@ public class DPoPAuthenticationConfigurerTests {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenCustomSuccessHandlerIsPresentThenAccessed() throws Exception {
|
||||
this.spring.register(SecurityConfigWithCustomSuccessHandler.class, ResourceEndpoints.class).autowire();
|
||||
Set<String> scope = Collections.singleton("resource1.read");
|
||||
String accessToken = generateAccessToken(scope, CLIENT_EC_KEY);
|
||||
String dPoPProof = generateDPoPProof(HttpMethod.GET.name(), "http://localhost/resource1", accessToken);
|
||||
// @formatter:off
|
||||
this.mvc.perform(get("/resource1")
|
||||
.header(HttpHeaders.AUTHORIZATION, "DPoP " + accessToken)
|
||||
.header("DPoP", dPoPProof))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string("resource1"));
|
||||
// @formatter:on
|
||||
verify(SecurityConfigWithCustomSuccessHandler.successHandler).onAuthenticationSuccess(any(), any(), any());
|
||||
}
|
||||
|
||||
private static String generateAccessToken(Set<String> scope, JWK jwk) {
|
||||
Map<String, Object> jktClaim = null;
|
||||
if (jwk != null) {
|
||||
@ -245,8 +268,11 @@ public class DPoPAuthenticationConfigurerTests {
|
||||
.requestMatchers("/resource2").hasAnyAuthority("SCOPE_resource2.read", "SCOPE_resource2.write")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer((oauth2) -> oauth2
|
||||
.jwt(Customizer.withDefaults()));
|
||||
.oauth2ResourceServer((oauth2ResourceServer) ->
|
||||
oauth2ResourceServer
|
||||
.jwt(Customizer.withDefaults())
|
||||
.dpop(Customizer.withDefaults())
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
@ -258,6 +284,48 @@ public class DPoPAuthenticationConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
static class SecurityConfigWithCustomSuccessHandler {
|
||||
|
||||
static final CustomSuccessHandler successHandler = spy(CustomSuccessHandler.class);
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) ->
|
||||
authorize
|
||||
.requestMatchers("/resource1").hasAnyAuthority("SCOPE_resource1.read", "SCOPE_resource1.write")
|
||||
.requestMatchers("/resource2").hasAnyAuthority("SCOPE_resource2.read", "SCOPE_resource2.write")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer((oauth2ResourceServer) ->
|
||||
oauth2ResourceServer
|
||||
.jwt(Customizer.withDefaults())
|
||||
.dpop((dpop) -> dpop.successHandler(successHandler))
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
NimbusJwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withPublicKey(PROVIDER_RSA_PUBLIC_KEY).build();
|
||||
}
|
||||
|
||||
static class CustomSuccessHandler implements AuthenticationSuccessHandler {
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class ResourceEndpoints {
|
||||
|
||||
|
||||
@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.config.annotation.web.oauth2.resourceserver
|
||||
|
||||
import com.nimbusds.jose.jwk.ECKey
|
||||
import com.nimbusds.jose.jwk.JWK
|
||||
import com.nimbusds.jose.jwk.JWKSelector
|
||||
import com.nimbusds.jose.jwk.JWKSet
|
||||
import com.nimbusds.jose.jwk.source.JWKSource
|
||||
import com.nimbusds.jose.proc.SecurityContext
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames
|
||||
import org.springframework.security.oauth2.jose.TestJwks
|
||||
import org.springframework.security.oauth2.jose.TestKeys
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm
|
||||
import org.springframework.security.oauth2.jwt.*
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestMethod
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import java.security.interfaces.ECPrivateKey
|
||||
import java.security.interfaces.ECPublicKey
|
||||
import java.security.interfaces.RSAPrivateKey
|
||||
import java.security.interfaces.RSAPublicKey
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Tests for [DPoPDsl]
|
||||
*
|
||||
* @author Max Batischev
|
||||
*/
|
||||
class DPoPDslTests {
|
||||
@JvmField
|
||||
val spring = SpringTestContext(this)
|
||||
|
||||
@Autowired
|
||||
lateinit var mockMvc: MockMvc
|
||||
|
||||
@Test
|
||||
fun requestWhenDPoPAndBearerAuthenticationThenUnauthorized() {
|
||||
spring.register(SecurityConfig::class.java, ResourceEndpoints::class.java).autowire()
|
||||
val scope = setOf("resource1.read")
|
||||
val accessToken = generateAccessToken(scope, CLIENT_EC_KEY)
|
||||
val dPoPProof = generateDPoPProof(HttpMethod.GET.name(), "http://localhost/resource1", accessToken)
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(MockMvcRequestBuilders.get("/resource1")
|
||||
.header(HttpHeaders.AUTHORIZATION, "DPoP $accessToken")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer $accessToken")
|
||||
.header("DPoP", dPoPProof))
|
||||
.andExpect(MockMvcResultMatchers.status().isUnauthorized())
|
||||
.andExpect(MockMvcResultMatchers.header().string(HttpHeaders.WWW_AUTHENTICATE,
|
||||
"DPoP error=\"invalid_request\", error_description=\"Found multiple Authorization headers.\", algs=\"RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES384 ES512\""))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestWhenDPoPAccessTokenMalformedThenUnauthorized() {
|
||||
spring.register(SecurityConfig::class.java, ResourceEndpoints::class.java).autowire()
|
||||
val scope = setOf("resource1.read")
|
||||
val accessToken = generateAccessToken(scope, CLIENT_EC_KEY)
|
||||
val dPoPProof = generateDPoPProof(HttpMethod.GET.name(), "http://localhost/resource1", accessToken)
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(MockMvcRequestBuilders.get("/resource1")
|
||||
.header(HttpHeaders.AUTHORIZATION, "DPoP $accessToken m a l f o r m e d ")
|
||||
.header("DPoP", dPoPProof))
|
||||
.andExpect(MockMvcResultMatchers.status().isUnauthorized())
|
||||
.andExpect(MockMvcResultMatchers.header().string(HttpHeaders.WWW_AUTHENTICATE,
|
||||
"DPoP error=\"invalid_token\", error_description=\"DPoP access token is malformed.\", algs=\"RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES384 ES512\""))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestWhenMultipleDPoPProofsThenUnauthorized() {
|
||||
spring.register(SecurityConfig::class.java, ResourceEndpoints::class.java).autowire()
|
||||
val scope = setOf("resource1.read")
|
||||
val accessToken = generateAccessToken(scope, CLIENT_EC_KEY)
|
||||
val dPoPProof = generateDPoPProof(HttpMethod.GET.name(), "http://localhost/resource1", accessToken)
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(MockMvcRequestBuilders.get("/resource1")
|
||||
.header(HttpHeaders.AUTHORIZATION, "DPoP $accessToken")
|
||||
.header("DPoP", dPoPProof)
|
||||
.header("DPoP", dPoPProof))
|
||||
.andExpect(MockMvcResultMatchers.status().isUnauthorized())
|
||||
.andExpect(MockMvcResultMatchers.header().string(HttpHeaders.WWW_AUTHENTICATE,
|
||||
"DPoP error=\"invalid_request\", error_description=\"DPoP proof is missing or invalid.\", algs=\"RS256 RS384 RS512 PS256 PS384 PS512 ES256 ES384 ES512\""))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestWhenDPoPAuthenticationValidThenAccessed() {
|
||||
spring.register(SecurityConfig::class.java, ResourceEndpoints::class.java).autowire()
|
||||
val scope = setOf("resource1.read")
|
||||
val accessToken = generateAccessToken(scope, CLIENT_EC_KEY)
|
||||
val dPoPProof = generateDPoPProof(HttpMethod.GET.name(), "http://localhost/resource1", accessToken)
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(MockMvcRequestBuilders.get("/resource1")
|
||||
.header(HttpHeaders.AUTHORIZATION, "DPoP $accessToken")
|
||||
.header("DPoP", dPoPProof))
|
||||
.andExpect(MockMvcResultMatchers.status().isOk())
|
||||
.andExpect(MockMvcResultMatchers.content().string("resource1"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private fun generateAccessToken(scope: Set<String>, jwk: JWK?): String {
|
||||
var jktClaim: MutableMap<String?, Any?>? = null
|
||||
if (jwk != null) {
|
||||
try {
|
||||
val sha256Thumbprint = jwk.toPublicJWK().computeThumbprint().toString()
|
||||
jktClaim = HashMap()
|
||||
jktClaim["jkt"] = sha256Thumbprint
|
||||
} catch (ignored: java.lang.Exception) {
|
||||
}
|
||||
}
|
||||
val jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256).build()
|
||||
val issuedAt = Instant.now()
|
||||
val expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES)
|
||||
// @formatter:off
|
||||
val claimsBuilder = JwtClaimsSet.builder()
|
||||
.issuer("https://provider.com")
|
||||
.subject("subject")
|
||||
.issuedAt(issuedAt)
|
||||
.expiresAt(expiresAt)
|
||||
.id(UUID.randomUUID().toString())
|
||||
.claim(OAuth2ParameterNames.SCOPE, scope)
|
||||
if (jktClaim != null) {
|
||||
claimsBuilder.claim("cnf", jktClaim) // Bind client public key
|
||||
}
|
||||
// @formatter:on
|
||||
val jwt = providerJwtEncoder!!.encode(JwtEncoderParameters.from(jwsHeader, claimsBuilder.build()))
|
||||
return jwt.tokenValue
|
||||
}
|
||||
|
||||
private fun generateDPoPProof(method: String, resourceUri: String, accessToken: String): String {
|
||||
// @formatter:off
|
||||
val publicJwk = CLIENT_EC_KEY.toPublicJWK().toJSONObject()
|
||||
val jwsHeader = JwsHeader.with(SignatureAlgorithm.ES256)
|
||||
.type("dpop+jwt")
|
||||
.jwk(publicJwk)
|
||||
.build()
|
||||
val claims = JwtClaimsSet.builder()
|
||||
.issuedAt(Instant.now())
|
||||
.claim("htm", method)
|
||||
.claim("htu", resourceUri)
|
||||
.claim("ath", computeSHA256(accessToken))
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build()
|
||||
// @formatter:on
|
||||
val jwt = clientJwtEncoder!!.encode(JwtEncoderParameters.from(jwsHeader, claims))
|
||||
return jwt.tokenValue
|
||||
}
|
||||
|
||||
private fun computeSHA256(value: String): String {
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
val digest = md.digest(value.toByteArray(StandardCharsets.UTF_8))
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest)
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableWebMvc
|
||||
internal open class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize("/resource1", hasAnyAuthority("SCOPE_resource1.read", "SCOPE_resource1.write"))
|
||||
authorize("/resource2", hasAnyAuthority("SCOPE_resource2.read", "SCOPE_resource2.write"))
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
oauth2ResourceServer {
|
||||
jwt { }
|
||||
dpop { }
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun jwtDecoder(): NimbusJwtDecoder = NimbusJwtDecoder.withPublicKey(PROVIDER_RSA_PUBLIC_KEY).build()
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
internal class ResourceEndpoints {
|
||||
@RequestMapping(value = ["/resource1"], method = [RequestMethod.GET, RequestMethod.POST])
|
||||
fun resource1(): String {
|
||||
return "resource1"
|
||||
}
|
||||
|
||||
@RequestMapping(value = ["/resource2"], method = [RequestMethod.GET, RequestMethod.POST])
|
||||
fun resource2(): String {
|
||||
return "resource2"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val PROVIDER_RSA_PUBLIC_KEY: RSAPublicKey = TestKeys.DEFAULT_PUBLIC_KEY
|
||||
|
||||
private val PROVIDER_RSA_PRIVATE_KEY: RSAPrivateKey = TestKeys.DEFAULT_PRIVATE_KEY
|
||||
|
||||
private val CLIENT_EC_PUBLIC_KEY = TestKeys.DEFAULT_EC_KEY_PAIR.public as ECPublicKey
|
||||
|
||||
private val CLIENT_EC_PRIVATE_KEY = TestKeys.DEFAULT_EC_KEY_PAIR.private as ECPrivateKey
|
||||
|
||||
private val CLIENT_EC_KEY: ECKey = TestJwks.jwk(CLIENT_EC_PUBLIC_KEY, CLIENT_EC_PRIVATE_KEY).build()
|
||||
|
||||
private var providerJwtEncoder: NimbusJwtEncoder? = null
|
||||
|
||||
private var clientJwtEncoder: NimbusJwtEncoder? = null
|
||||
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun init() {
|
||||
val providerRsaKey = TestJwks.jwk(PROVIDER_RSA_PUBLIC_KEY, PROVIDER_RSA_PRIVATE_KEY).build()
|
||||
val providerJwkSource = JWKSource { jwkSelector: JWKSelector, _: SecurityContext? ->
|
||||
jwkSelector
|
||||
.select(JWKSet(providerRsaKey))
|
||||
}
|
||||
providerJwtEncoder = NimbusJwtEncoder(providerJwkSource)
|
||||
val clientJwkSource = JWKSource { jwkSelector: JWKSelector, securityContext: SecurityContext? ->
|
||||
jwkSelector
|
||||
.select(JWKSet(CLIENT_EC_KEY))
|
||||
}
|
||||
clientJwtEncoder = NimbusJwtEncoder(clientJwkSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.resource.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
public final class DPoPAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
private static final Pattern AUTHORIZATION_PATTERN = Pattern.compile("^DPoP (?<token>[a-zA-Z0-9-._~+/]+=*)$",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@Override
|
||||
public @Nullable 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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.resource.web;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
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.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
public 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 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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.resource.web;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
public 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());
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user