Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b47d5ebb72 | ||
|
|
1a6a9dd1f2 | ||
|
|
836acf7c0e | ||
|
|
32c53e53da | ||
|
|
5bb43af3ff | ||
|
|
6dc3944eef | ||
|
|
4eb25c163f | ||
|
|
356d669a78 | ||
|
|
11ce8ef201 | ||
|
|
bfd7a09c3b | ||
|
|
efbfdc234c | ||
|
|
2ba711c83a | ||
|
|
8d7f8b3420 | ||
|
|
8c2b095195 | ||
|
|
b1b2bc438f | ||
|
|
72804be45b | ||
|
|
629e220c2f | ||
|
|
9c964e37b0 | ||
|
|
d7aa72af68 | ||
|
|
a9cf857d33 | ||
|
|
3729dc0d43 | ||
|
|
dc142cb253 | ||
|
|
9c0ca08e68 | ||
|
|
1f68ad1655 | ||
|
|
b71801cd1e | ||
|
|
213bf49510 | ||
|
|
f114e3a1ce | ||
|
|
17c882c06d |
@@ -4,7 +4,7 @@ image:https://github.com/spring-projects/spring-authorization-server/workflows/C
|
||||
|
||||
= Spring Authorization Server
|
||||
|
||||
The Spring Authorization Server project, led by the https://spring.io/projects/spring-security/[Spring Security] team, is focused on delivering https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-1.1[OAuth 2.1 Authorization Server] support to the Spring community.
|
||||
The Spring Authorization Server project, led by the https://spring.io/projects/spring-security/[Spring Security] team, is focused on delivering https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-06#section-1.1[OAuth 2.1 Authorization Server] support to the Spring community.
|
||||
|
||||
This project replaces the Authorization Server support provided by https://spring.io/projects/spring-security-oauth/[Spring Security OAuth].
|
||||
|
||||
|
||||
@@ -23,5 +23,5 @@ dependencies {
|
||||
implementation "org.hidetake:gradle-ssh-plugin:2.10.1"
|
||||
implementation "org.jfrog.buildinfo:build-info-extractor-gradle:4.26.1"
|
||||
implementation "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1"
|
||||
implementation "org.springframework:spring-core:5.3.22"
|
||||
implementation "org.springframework:spring-core:5.3.23"
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@ javaPlatform {
|
||||
dependencies {
|
||||
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
|
||||
api platform("org.springframework.security:spring-security-bom:$springSecurityVersion")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.3")
|
||||
api platform("com.fasterxml.jackson:jackson-bom:2.13.4.20221013")
|
||||
constraints {
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.23"
|
||||
api "com.nimbusds:nimbus-jose-jwt:9.24.4"
|
||||
api "javax.servlet:javax.servlet-api:4.0.1"
|
||||
api "junit:junit:4.13.2"
|
||||
api "org.assertj:assertj-core:3.22.0"
|
||||
api "org.mockito:mockito-core:4.5.1"
|
||||
api "com.squareup.okhttp3:mockwebserver:4.9.3"
|
||||
api "com.squareup.okhttp3:okhttp:4.9.3"
|
||||
api "org.assertj:assertj-core:3.23.1"
|
||||
api "org.mockito:mockito-core:4.8.1"
|
||||
api "com.squareup.okhttp3:mockwebserver:4.10.0"
|
||||
api "com.squareup.okhttp3:okhttp:4.10.0"
|
||||
api "com.jayway.jsonpath:json-path:2.7.0"
|
||||
api "org.hsqldb:hsqldb:2.5.2"
|
||||
}
|
||||
|
||||
@@ -19,15 +19,10 @@ The OAuth2 authorization server `SecurityFilterChain` `@Bean` is configured with
|
||||
* xref:protocol-endpoints.adoc#oauth2-token-revocation-endpoint[OAuth2 Token Revocation endpoint]
|
||||
* xref:protocol-endpoints.adoc#oauth2-authorization-server-metadata-endpoint[OAuth2 Authorization Server Metadata endpoint]
|
||||
* xref:protocol-endpoints.adoc#jwk-set-endpoint[JWK Set endpoint]
|
||||
* xref:protocol-endpoints.adoc#oidc-provider-configuration-endpoint[OpenID Connect 1.0 Provider Configuration endpoint]
|
||||
* xref:protocol-endpoints.adoc#oidc-user-info-endpoint[OpenID Connect 1.0 UserInfo endpoint]
|
||||
|
||||
[NOTE]
|
||||
The JWK Set endpoint is configured *only* if a `JWKSource<SecurityContext>` `@Bean` is registered.
|
||||
|
||||
[NOTE]
|
||||
The xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration endpoint] is disabled by default because many deployments do not require dynamic client registration.
|
||||
|
||||
The following example shows how to use `OAuth2AuthorizationServerConfiguration` to apply the minimal default configuration:
|
||||
|
||||
[source,java]
|
||||
@@ -55,6 +50,29 @@ public class AuthorizationServerConfig {
|
||||
[IMPORTANT]
|
||||
The https://datatracker.ietf.org/doc/html/rfc6749#section-4.1[authorization_code grant] requires the resource owner to be authenticated. Therefore, a user authentication mechanism *must* be configured in addition to the default OAuth2 security configuration.
|
||||
|
||||
https://openid.net/specs/openid-connect-core-1_0.html[OpenID Connect 1.0] is disabled in the default configuration. The following example shows how to enable OpenID Connect 1.0 by initializing the `OidcConfigurer`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.oidc(Customizer.withDefaults()); // Initialize `OidcConfigurer`
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
In addition to the default protocol endpoints, the OAuth2 authorization server `SecurityFilterChain` `@Bean` is configured with the following OpenID Connect 1.0 protocol endpoints:
|
||||
|
||||
* xref:protocol-endpoints.adoc#oidc-provider-configuration-endpoint[OpenID Connect 1.0 Provider Configuration endpoint]
|
||||
* xref:protocol-endpoints.adoc#oidc-user-info-endpoint[OpenID Connect 1.0 UserInfo endpoint]
|
||||
|
||||
[NOTE]
|
||||
The xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration endpoint] is disabled by default because many deployments do not require dynamic client registration.
|
||||
|
||||
[TIP]
|
||||
`OAuth2AuthorizationServerConfiguration.jwtDecoder(JWKSource<SecurityContext>)` is a convenience (`static`) utility method that can be used to register a `JwtDecoder` `@Bean`, which is *REQUIRED* for the xref:protocol-endpoints.adoc#oidc-user-info-endpoint[OpenID Connect 1.0 UserInfo endpoint] and the xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration endpoint].
|
||||
|
||||
@@ -98,9 +116,11 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
.tokenEndpoint(tokenEndpoint -> { }) <8>
|
||||
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint -> { }) <9>
|
||||
.tokenRevocationEndpoint(tokenRevocationEndpoint -> { }) <10>
|
||||
.authorizationServerMetadataEndpoint(authorizationServerMetadataEndpoint -> { }) <11>
|
||||
.oidc(oidc -> oidc
|
||||
.userInfoEndpoint(userInfoEndpoint -> { }) <11>
|
||||
.clientRegistrationEndpoint(clientRegistrationEndpoint -> { }) <12>
|
||||
.providerConfigurationEndpoint(providerConfigurationEndpoint -> { }) <12>
|
||||
.userInfoEndpoint(userInfoEndpoint -> { }) <13>
|
||||
.clientRegistrationEndpoint(clientRegistrationEndpoint -> { }) <14>
|
||||
);
|
||||
|
||||
return http.build();
|
||||
@@ -116,8 +136,10 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
<8> `tokenEndpoint()`: The configurer for the xref:protocol-endpoints.adoc#oauth2-token-endpoint[OAuth2 Token endpoint].
|
||||
<9> `tokenIntrospectionEndpoint()`: The configurer for the xref:protocol-endpoints.adoc#oauth2-token-introspection-endpoint[OAuth2 Token Introspection endpoint].
|
||||
<10> `tokenRevocationEndpoint()`: The configurer for the xref:protocol-endpoints.adoc#oauth2-token-revocation-endpoint[OAuth2 Token Revocation endpoint].
|
||||
<11> `userInfoEndpoint()`: The configurer for the xref:protocol-endpoints.adoc#oidc-user-info-endpoint[OpenID Connect 1.0 UserInfo endpoint].
|
||||
<12> `clientRegistrationEndpoint()`: The configurer for the xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration endpoint].
|
||||
<11> `authorizationServerMetadataEndpoint()`: The configurer for the xref:protocol-endpoints.adoc#oauth2-authorization-server-metadata-endpoint[OAuth2 Authorization Server Metadata endpoint].
|
||||
<12> `providerConfigurationEndpoint()`: The configurer for the xref:protocol-endpoints.adoc#oidc-provider-configuration-endpoint[OpenID Connect 1.0 Provider Configuration endpoint].
|
||||
<13> `userInfoEndpoint()`: The configurer for the xref:protocol-endpoints.adoc#oidc-user-info-endpoint[OpenID Connect 1.0 UserInfo endpoint].
|
||||
<14> `clientRegistrationEndpoint()`: The configurer for the xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration endpoint].
|
||||
|
||||
[[configuring-authorization-server-settings]]
|
||||
== Configuring Authorization Server Settings
|
||||
|
||||
@@ -11,7 +11,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation platform("org.springframework.boot:spring-boot-dependencies:2.7.0")
|
||||
implementation platform("org.springframework.boot:spring-boot-dependencies:2.7.5")
|
||||
implementation "org.springframework.boot:spring-boot-starter-web"
|
||||
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
|
||||
implementation "org.springframework.boot:spring-boot-starter-security"
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.springframework.security.oauth2.server.authorization.client.InMemoryR
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
@@ -56,6 +57,8 @@ public class SecurityConfig {
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
|
||||
throws Exception {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
||||
// @formatter:off
|
||||
http
|
||||
// Redirect to the login page when not authenticated from the
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.springframework.security.oauth2.server.authorization.client.InMemoryR
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
@@ -57,6 +58,8 @@ public class EnableUserInfoSecurityConfig {
|
||||
@Order(1)
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
||||
// @formatter:off
|
||||
http
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) // <2>
|
||||
|
||||
@@ -66,7 +66,6 @@ public class OidcUserInfoService {
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.phoneNumberVerified("false")
|
||||
.claim("address", Collections.singletonMap("formatted", "Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"))
|
||||
.updatedAt("1970-01-01T00:00:00Z")
|
||||
.build()
|
||||
|
||||
@@ -36,8 +36,12 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.Customizer;
|
||||
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.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
@@ -49,6 +53,10 @@ import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -133,9 +141,25 @@ public class JpaTests {
|
||||
@EnableWebSecurity
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfig {
|
||||
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.exceptionHandling(exceptions ->
|
||||
exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
|
||||
)
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JWKSource<SecurityContext> jwkSource() {
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
@@ -147,6 +171,11 @@ public class JpaTests {
|
||||
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthorizationServerSettings authorizationServerSettings() {
|
||||
return AuthorizationServerSettings.builder().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -127,7 +127,6 @@ public class EnableUserInfoSecurityConfigTests {
|
||||
jsonPath("zoneinfo").value("Europe/Paris"),
|
||||
jsonPath("locale").value("en-US"),
|
||||
jsonPath("phone_number").value("+1 (604) 555-1234;ext=5678"),
|
||||
jsonPath("phone_number_verified").value("false"),
|
||||
jsonPath("address.formatted").value("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"),
|
||||
jsonPath("updated_at").value("1970-01-01T00:00:00Z")
|
||||
);
|
||||
|
||||
@@ -269,9 +269,9 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
== OpenID Connect 1.0 UserInfo Endpoint
|
||||
|
||||
`OidcUserInfoEndpointConfigurer` provides the ability to customize the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[OpenID Connect 1.0 UserInfo endpoint].
|
||||
It defines extension points that let you customize the https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse[UserInfo response].
|
||||
It defines extension points that let you customize the pre-processing, main processing, and post-processing logic for https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest[UserInfo requests].
|
||||
|
||||
`OidcUserInfoEndpointConfigurer` provides the following configuration option:
|
||||
`OidcUserInfoEndpointConfigurer` provides the following configuration options:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@@ -285,21 +285,37 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
.oidc(oidc ->
|
||||
oidc
|
||||
.userInfoEndpoint(userInfoEndpoint ->
|
||||
userInfoEndpoint.userInfoMapper(userInfoMapper) <1>
|
||||
userInfoEndpoint
|
||||
.userInfoRequestConverter(userInfoRequestConverter) <1>
|
||||
.userInfoRequestConverters(userInfoRequestConvertersConsumer) <2>
|
||||
.authenticationProvider(authenticationProvider) <3>
|
||||
.authenticationProviders(authenticationProvidersConsumer) <4>
|
||||
.userInfoResponseHandler(userInfoResponseHandler) <5>
|
||||
.errorResponseHandler(errorResponseHandler) <6>
|
||||
.userInfoMapper(userInfoMapper) <7>
|
||||
)
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
<1> `userInfoMapper()`: The `Function` used to extract claims from `OidcUserInfoAuthenticationContext` to an instance of `OidcUserInfo`.
|
||||
<1> `userInfoRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest[UserInfo request] from `HttpServletRequest` to an instance of `OidcUserInfoAuthenticationToken`.
|
||||
<2> `userInfoRequestConverters()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationConverter``'s allowing the ability to add, remove, or customize a specific `AuthenticationConverter`.
|
||||
<3> `authenticationProvider()`: Adds an `AuthenticationProvider` (_main processor_) used for authenticating the `OidcUserInfoAuthenticationToken`.
|
||||
<4> `authenticationProviders()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationProvider``'s allowing the ability to add, remove, or customize a specific `AuthenticationProvider`.
|
||||
<5> `userInfoResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OidcUserInfoAuthenticationToken` and returning the https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse[UserInfo response].
|
||||
<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://openid.net/specs/openid-connect-core-1_0.html#UserInfoError[UserInfo Error response].
|
||||
<7> `userInfoMapper()`: The `Function` used to extract claims from `OidcUserInfoAuthenticationContext` to an instance of `OidcUserInfo`.
|
||||
|
||||
`OidcUserInfoEndpointConfigurer` configures the `OidcUserInfoEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OidcUserInfoEndpointFilter` is the `Filter` that processes https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest[UserInfo requests] and returns the https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse[OidcUserInfo response].
|
||||
|
||||
`OidcUserInfoEndpointFilter` is configured with the following defaults:
|
||||
|
||||
* `*AuthenticationConverter*` -- An internal implementation that obtains the `Authentication` from the `SecurityContext` and creates an `OidcUserInfoAuthenticationToken` with the principal.
|
||||
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OidcUserInfoAuthenticationProvider`, which is associated with an internal implementation of `userInfoMapper` that extracts https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims[standard claims] from the https://openid.net/specs/openid-connect-core-1_0.html#IDToken[ID Token] based on the https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims[scopes requested] during authorization.
|
||||
* `*AuthenticationSuccessHandler*` -- An internal implementation that handles an "`authenticated`" `OidcUserInfoAuthenticationToken` and returns the `OidcUserInfo` response.
|
||||
* `*AuthenticationFailureHandler*` -- An internal implementation that uses the `OAuth2Error` associated with the `OAuth2AuthenticationException` and returns the `OAuth2Error` response.
|
||||
|
||||
[TIP]
|
||||
You can customize the ID Token by providing an xref:core-model-components.adoc#oauth2-token-customizer[`OAuth2TokenCustomizer<JwtEncodingContext>`] `@Bean`.
|
||||
@@ -337,8 +353,10 @@ The guide xref:guides/how-to-userinfo.adoc#how-to-userinfo[How-to: Customize the
|
||||
[[oidc-client-registration-endpoint]]
|
||||
== OpenID Connect 1.0 Client Registration Endpoint
|
||||
|
||||
`OidcClientRegistrationEndpointConfigurer` configures the https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration[OpenID Connect 1.0 Client Registration endpoint].
|
||||
The following example shows how to enable (disabled by default) the OpenID Connect 1.0 Client Registration endpoint:
|
||||
`OidcClientRegistrationEndpointConfigurer` provides the ability to customize the https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration[OpenID Connect 1.0 Client Registration endpoint].
|
||||
It defines extension points that let you customize the pre-processing, main processing, and post-processing logic for https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationRequest[Client Registration requests] or https://openid.net/specs/openid-connect-registration-1_0.html#ReadRequest[Client Read requests].
|
||||
|
||||
`OidcClientRegistrationEndpointConfigurer` provides the following configuration options:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@@ -351,12 +369,26 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
authorizationServerConfigurer
|
||||
.oidc(oidc ->
|
||||
oidc
|
||||
.clientRegistrationEndpoint(Customizer.withDefaults())
|
||||
.clientRegistrationEndpoint(clientRegistrationEndpoint ->
|
||||
clientRegistrationEndpoint
|
||||
.clientRegistrationRequestConverter(clientRegistrationRequestConverter) <1>
|
||||
.clientRegistrationRequestConverters(clientRegistrationRequestConvertersConsumers) <2>
|
||||
.authenticationProvider(authenticationProvider) <3>
|
||||
.authenticationProviders(authenticationProvidersConsumer) <4>
|
||||
.clientRegistrationResponseHandler(clientRegistrationResponseHandler) <5>
|
||||
.errorResponseHandler(errorResponseHandler) <6>
|
||||
)
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
<1> `clientRegistrationRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract a https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationRequest[Client Registration request] or https://openid.net/specs/openid-connect-registration-1_0.html#ReadRequest[Client Read request] from `HttpServletRequest` to an instance of `OidcClientRegistrationAuthenticationToken`.
|
||||
<2> `clientRegistrationRequestConverters()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationConverter``'s allowing the ability to add, remove, or customize a specific `AuthenticationConverter`.
|
||||
<3> `authenticationProvider()`: Adds an `AuthenticationProvider` (_main processor_) used for authenticating the `OidcClientRegistrationAuthenticationToken`.
|
||||
<4> `authenticationProviders()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationProvider``'s allowing the ability to add, remove, or customize a specific `AuthenticationProvider`.
|
||||
<5> `clientRegistrationResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OidcClientRegistrationAuthenticationToken` and returning the https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse[Client Registration response] or https://openid.net/specs/openid-connect-registration-1_0.html#ReadResponse[Client Read response].
|
||||
<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError[Client Registration Error response] or https://openid.net/specs/openid-connect-registration-1_0.html#ReadError[Client Read Error response].
|
||||
|
||||
[NOTE]
|
||||
The OpenID Connect 1.0 Client Registration endpoint is disabled by default because many deployments do not require dynamic client registration.
|
||||
@@ -369,7 +401,10 @@ The OpenID Connect 1.0 Client Registration endpoint is disabled by default becau
|
||||
|
||||
`OidcClientRegistrationEndpointFilter` is configured with the following defaults:
|
||||
|
||||
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OidcClientRegistrationAuthenticationProvider`.
|
||||
* `*AuthenticationConverter*` -- An `OidcClientRegistrationAuthenticationConverter`.
|
||||
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OidcClientRegistrationAuthenticationProvider` and `OidcClientConfigurationAuthenticationProvider`.
|
||||
* `*AuthenticationSuccessHandler*` -- An internal implementation that handles an "`authenticated`" `OidcClientRegistrationAuthenticationToken` and returns the `OidcClientRegistration` response.
|
||||
* `*AuthenticationFailureHandler*` -- An internal implementation that uses the `OAuth2Error` associated with the `OAuth2AuthenticationException` and returns the `OAuth2Error` response.
|
||||
|
||||
The OpenID Connect 1.0 Client Registration endpoint is an https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration[OAuth2 protected resource], which *REQUIRES* an access token to be sent as a bearer token in the Client Registration (or Client Read) request.
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
version=0.4.0-SNAPSHOT
|
||||
version=0.4.0-RC1
|
||||
org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
springFrameworkVersion=5.3.22
|
||||
springSecurityVersion=5.8.0-M2
|
||||
springFrameworkVersion=5.3.23
|
||||
springSecurityVersion=5.8.0-RC1
|
||||
springJavaformatVersion=0.0.31
|
||||
springJavaformatExcludePackages=org/springframework/security/config org/springframework/security/oauth2
|
||||
checkstyleToolVersion=8.34
|
||||
|
||||
@@ -15,53 +15,27 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
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.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
|
||||
import org.springframework.security.oauth2.jwt.JwtException;
|
||||
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation used for OAuth 2.0 Client Authentication,
|
||||
* which authenticates the (JWT) {@link OAuth2ParameterNames#CLIENT_ASSERTION client_assertion} parameter.
|
||||
* which authenticates the {@link Jwt} {@link OAuth2ParameterNames#CLIENT_ASSERTION client_assertion} parameter.
|
||||
*
|
||||
* @author Rafal Lewczuk
|
||||
* @author Joe Grandja
|
||||
@@ -70,6 +44,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see JwtClientAssertionDecoderFactory
|
||||
*/
|
||||
public final class JwtClientAssertionAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
|
||||
@@ -77,7 +52,7 @@ public final class JwtClientAssertionAuthenticationProvider implements Authentic
|
||||
new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final CodeVerifierAuthenticator codeVerifierAuthenticator;
|
||||
private final JwtClientAssertionDecoderFactory jwtClientAssertionDecoderFactory;
|
||||
private JwtDecoderFactory<RegisteredClient> jwtDecoderFactory;
|
||||
|
||||
/**
|
||||
* Constructs a {@code JwtClientAssertionAuthenticationProvider} using the provided parameters.
|
||||
@@ -91,7 +66,7 @@ public final class JwtClientAssertionAuthenticationProvider implements Authentic
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
|
||||
this.jwtClientAssertionDecoderFactory = new JwtClientAssertionDecoderFactory();
|
||||
this.jwtDecoderFactory = new JwtClientAssertionDecoderFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -119,7 +94,7 @@ public final class JwtClientAssertionAuthenticationProvider implements Authentic
|
||||
}
|
||||
|
||||
Jwt jwtAssertion = null;
|
||||
JwtDecoder jwtDecoder = this.jwtClientAssertionDecoderFactory.createDecoder(registeredClient);
|
||||
JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(registeredClient);
|
||||
try {
|
||||
jwtAssertion = jwtDecoder.decode(clientAuthentication.getCredentials().toString());
|
||||
} catch (JwtException ex) {
|
||||
@@ -142,6 +117,19 @@ public final class JwtClientAssertionAuthenticationProvider implements Authentic
|
||||
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link JwtDecoderFactory} that provides a {@link JwtDecoder} for the specified {@link RegisteredClient}
|
||||
* and is used for authenticating a {@link Jwt} Bearer Token during OAuth 2.0 Client Authentication.
|
||||
* The default factory is {@link JwtClientAssertionDecoderFactory}.
|
||||
*
|
||||
* @param jwtDecoderFactory the {@link JwtDecoderFactory} that provides a {@link JwtDecoder} for the specified {@link RegisteredClient}
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public void setJwtDecoderFactory(JwtDecoderFactory<RegisteredClient> jwtDecoderFactory) {
|
||||
Assert.notNull(jwtDecoderFactory, "jwtDecoderFactory cannot be null");
|
||||
this.jwtDecoderFactory = jwtDecoderFactory;
|
||||
}
|
||||
|
||||
private static void throwInvalidClient(String parameterName) {
|
||||
throwInvalidClient(parameterName, null);
|
||||
}
|
||||
@@ -155,112 +143,4 @@ public final class JwtClientAssertionAuthenticationProvider implements Authentic
|
||||
throw new OAuth2AuthenticationException(error, error.toString(), cause);
|
||||
}
|
||||
|
||||
private static class JwtClientAssertionDecoderFactory implements JwtDecoderFactory<RegisteredClient> {
|
||||
private static final String JWT_CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7523#section-3";
|
||||
|
||||
private static final Map<JwsAlgorithm, String> JCA_ALGORITHM_MAPPINGS;
|
||||
|
||||
static {
|
||||
Map<JwsAlgorithm, String> mappings = new HashMap<>();
|
||||
mappings.put(MacAlgorithm.HS256, "HmacSHA256");
|
||||
mappings.put(MacAlgorithm.HS384, "HmacSHA384");
|
||||
mappings.put(MacAlgorithm.HS512, "HmacSHA512");
|
||||
JCA_ALGORITHM_MAPPINGS = Collections.unmodifiableMap(mappings);
|
||||
}
|
||||
|
||||
private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public JwtDecoder createDecoder(RegisteredClient registeredClient) {
|
||||
Assert.notNull(registeredClient, "registeredClient cannot be null");
|
||||
return this.jwtDecoders.computeIfAbsent(registeredClient.getId(), (key) -> {
|
||||
NimbusJwtDecoder jwtDecoder = buildDecoder(registeredClient);
|
||||
jwtDecoder.setJwtValidator(createJwtValidator(registeredClient));
|
||||
return jwtDecoder;
|
||||
});
|
||||
}
|
||||
|
||||
private static NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) {
|
||||
JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm();
|
||||
if (jwsAlgorithm instanceof SignatureAlgorithm) {
|
||||
String jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl();
|
||||
if (!StringUtils.hasText(jwkSetUrl)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Failed to find a Signature Verifier for Client: '"
|
||||
+ registeredClient.getId()
|
||||
+ "'. Check to ensure you have configured the JWK Set URL.",
|
||||
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(oauth2Error);
|
||||
}
|
||||
return NimbusJwtDecoder.withJwkSetUri(jwkSetUrl).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();
|
||||
}
|
||||
if (jwsAlgorithm instanceof MacAlgorithm) {
|
||||
String clientSecret = registeredClient.getClientSecret();
|
||||
if (!StringUtils.hasText(clientSecret)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Failed to find a Signature Verifier for Client: '"
|
||||
+ registeredClient.getId()
|
||||
+ "'. Check to ensure you have configured the client secret.",
|
||||
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(oauth2Error);
|
||||
}
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
|
||||
JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
|
||||
return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
|
||||
}
|
||||
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Failed to find a Signature Verifier for Client: '"
|
||||
+ registeredClient.getId()
|
||||
+ "'. Check to ensure you have configured a valid JWS Algorithm: '" + jwsAlgorithm + "'.",
|
||||
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(oauth2Error);
|
||||
}
|
||||
|
||||
private static OAuth2TokenValidator<Jwt> createJwtValidator(RegisteredClient registeredClient) {
|
||||
String clientId = registeredClient.getClientId();
|
||||
return new DelegatingOAuth2TokenValidator<>(
|
||||
new JwtClaimValidator<>(JwtClaimNames.ISS, clientId::equals),
|
||||
new JwtClaimValidator<>(JwtClaimNames.SUB, clientId::equals),
|
||||
new JwtClaimValidator<>(JwtClaimNames.AUD, containsAudience()),
|
||||
new JwtClaimValidator<>(JwtClaimNames.EXP, Objects::nonNull),
|
||||
new JwtTimestampValidator()
|
||||
);
|
||||
}
|
||||
|
||||
private static Predicate<List<String>> containsAudience() {
|
||||
return (audienceClaim) -> {
|
||||
if (CollectionUtils.isEmpty(audienceClaim)) {
|
||||
return false;
|
||||
}
|
||||
List<String> audienceList = getAudience();
|
||||
for (String audience : audienceClaim) {
|
||||
if (audienceList.contains(audience)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
private static List<String> getAudience() {
|
||||
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
|
||||
if (!StringUtils.hasText(authorizationServerContext.getIssuer())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings();
|
||||
List<String> audience = new ArrayList<>();
|
||||
audience.add(authorizationServerContext.getIssuer());
|
||||
audience.add(asUrl(authorizationServerContext.getIssuer(), authorizationServerSettings.getTokenEndpoint()));
|
||||
audience.add(asUrl(authorizationServerContext.getIssuer(), authorizationServerSettings.getTokenIntrospectionEndpoint()));
|
||||
audience.add(asUrl(authorizationServerContext.getIssuer(), authorizationServerSettings.getTokenRevocationEndpoint()));
|
||||
return audience;
|
||||
}
|
||||
|
||||
private static String asUrl(String issuer, String endpoint) {
|
||||
return UriComponentsBuilder.fromUriString(issuer).path(endpoint).build().toUriString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
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.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
|
||||
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A {@link JwtDecoderFactory factory} that provides a {@link JwtDecoder} for the specified {@link RegisteredClient}
|
||||
* and is used for authenticating a {@link Jwt} Bearer Token during OAuth 2.0 Client Authentication.
|
||||
*
|
||||
* @author Rafal Lewczuk
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see JwtDecoderFactory
|
||||
* @see RegisteredClient
|
||||
* @see OAuth2TokenValidator
|
||||
* @see JwtClientAssertionAuthenticationProvider
|
||||
* @see ClientAuthenticationMethod#PRIVATE_KEY_JWT
|
||||
* @see ClientAuthenticationMethod#CLIENT_SECRET_JWT
|
||||
*/
|
||||
public final class JwtClientAssertionDecoderFactory implements JwtDecoderFactory<RegisteredClient> {
|
||||
|
||||
/**
|
||||
* The default {@code OAuth2TokenValidator<Jwt>} factory that validates the {@link JwtClaimNames#ISS iss},
|
||||
* {@link JwtClaimNames#SUB sub}, {@link JwtClaimNames#AUD aud}, {@link JwtClaimNames#EXP exp} and
|
||||
* {@link JwtClaimNames#NBF nbf} claims of the {@link Jwt} for the specified {@link RegisteredClient}.
|
||||
*/
|
||||
public static final Function<RegisteredClient, OAuth2TokenValidator<Jwt>> DEFAULT_JWT_VALIDATOR_FACTORY = defaultJwtValidatorFactory();
|
||||
|
||||
private static final String JWT_CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7523#section-3";
|
||||
private static final Map<JwsAlgorithm, String> JCA_ALGORITHM_MAPPINGS;
|
||||
|
||||
static {
|
||||
Map<JwsAlgorithm, String> mappings = new HashMap<>();
|
||||
mappings.put(MacAlgorithm.HS256, "HmacSHA256");
|
||||
mappings.put(MacAlgorithm.HS384, "HmacSHA384");
|
||||
mappings.put(MacAlgorithm.HS512, "HmacSHA512");
|
||||
JCA_ALGORITHM_MAPPINGS = Collections.unmodifiableMap(mappings);
|
||||
}
|
||||
|
||||
private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
|
||||
private Function<RegisteredClient, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = DEFAULT_JWT_VALIDATOR_FACTORY;
|
||||
|
||||
@Override
|
||||
public JwtDecoder createDecoder(RegisteredClient registeredClient) {
|
||||
Assert.notNull(registeredClient, "registeredClient cannot be null");
|
||||
return this.jwtDecoders.computeIfAbsent(registeredClient.getId(), (key) -> {
|
||||
NimbusJwtDecoder jwtDecoder = buildDecoder(registeredClient);
|
||||
jwtDecoder.setJwtValidator(this.jwtValidatorFactory.apply(registeredClient));
|
||||
return jwtDecoder;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the factory that provides an {@link OAuth2TokenValidator}
|
||||
* for the specified {@link RegisteredClient} 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 RegisteredClient}
|
||||
*/
|
||||
public void setJwtValidatorFactory(Function<RegisteredClient, OAuth2TokenValidator<Jwt>> jwtValidatorFactory) {
|
||||
Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null");
|
||||
this.jwtValidatorFactory = jwtValidatorFactory;
|
||||
}
|
||||
|
||||
private static NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) {
|
||||
JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm();
|
||||
if (jwsAlgorithm instanceof SignatureAlgorithm) {
|
||||
String jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl();
|
||||
if (!StringUtils.hasText(jwkSetUrl)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Failed to find a Signature Verifier for Client: '"
|
||||
+ registeredClient.getId()
|
||||
+ "'. Check to ensure you have configured the JWK Set URL.",
|
||||
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(oauth2Error);
|
||||
}
|
||||
return NimbusJwtDecoder.withJwkSetUri(jwkSetUrl).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();
|
||||
}
|
||||
if (jwsAlgorithm instanceof MacAlgorithm) {
|
||||
String clientSecret = registeredClient.getClientSecret();
|
||||
if (!StringUtils.hasText(clientSecret)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Failed to find a Signature Verifier for Client: '"
|
||||
+ registeredClient.getId()
|
||||
+ "'. Check to ensure you have configured the client secret.",
|
||||
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(oauth2Error);
|
||||
}
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
|
||||
JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
|
||||
return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
|
||||
}
|
||||
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
"Failed to find a Signature Verifier for Client: '"
|
||||
+ registeredClient.getId()
|
||||
+ "'. Check to ensure you have configured a valid JWS Algorithm: '" + jwsAlgorithm + "'.",
|
||||
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(oauth2Error);
|
||||
}
|
||||
|
||||
private static Function<RegisteredClient, OAuth2TokenValidator<Jwt>> defaultJwtValidatorFactory() {
|
||||
return (registeredClient) -> {
|
||||
String clientId = registeredClient.getClientId();
|
||||
return new DelegatingOAuth2TokenValidator<>(
|
||||
new JwtClaimValidator<>(JwtClaimNames.ISS, clientId::equals),
|
||||
new JwtClaimValidator<>(JwtClaimNames.SUB, clientId::equals),
|
||||
new JwtClaimValidator<>(JwtClaimNames.AUD, containsAudience()),
|
||||
new JwtClaimValidator<>(JwtClaimNames.EXP, Objects::nonNull),
|
||||
new JwtTimestampValidator()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private static Predicate<List<String>> containsAudience() {
|
||||
return (audienceClaim) -> {
|
||||
if (CollectionUtils.isEmpty(audienceClaim)) {
|
||||
return false;
|
||||
}
|
||||
List<String> audienceList = getAudience();
|
||||
for (String audience : audienceClaim) {
|
||||
if (audienceList.contains(audience)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
private static List<String> getAudience() {
|
||||
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
|
||||
if (!StringUtils.hasText(authorizationServerContext.getIssuer())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings();
|
||||
List<String> audience = new ArrayList<>();
|
||||
audience.add(authorizationServerContext.getIssuer());
|
||||
audience.add(asUrl(authorizationServerContext.getIssuer(), authorizationServerSettings.getTokenEndpoint()));
|
||||
audience.add(asUrl(authorizationServerContext.getIssuer(), authorizationServerSettings.getTokenIntrospectionEndpoint()));
|
||||
audience.add(asUrl(authorizationServerContext.getIssuer(), authorizationServerSettings.getTokenRevocationEndpoint()));
|
||||
return audience;
|
||||
}
|
||||
|
||||
private static String asUrl(String issuer, String endpoint) {
|
||||
return UriComponentsBuilder.fromUriString(issuer).path(endpoint).build().toUriString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,9 +28,11 @@ import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationValidator;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
@@ -65,6 +67,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
private AuthenticationSuccessHandler authorizationResponseHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
private String consentPage;
|
||||
private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authorizationCodeRequestAuthenticationValidator;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
@@ -189,6 +192,14 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
return this;
|
||||
}
|
||||
|
||||
void addAuthorizationCodeRequestAuthenticationValidator(
|
||||
Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
|
||||
this.authorizationCodeRequestAuthenticationValidator =
|
||||
this.authorizationCodeRequestAuthenticationValidator == null ?
|
||||
authenticationValidator :
|
||||
this.authorizationCodeRequestAuthenticationValidator.andThen(authenticationValidator);
|
||||
}
|
||||
|
||||
@Override
|
||||
void init(HttpSecurity httpSecurity) {
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
@@ -251,7 +262,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
return authenticationConverters;
|
||||
}
|
||||
|
||||
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider =
|
||||
@@ -259,6 +270,11 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
|
||||
OAuth2ConfigurerUtils.getAuthorizationConsentService(httpSecurity));
|
||||
if (this.authorizationCodeRequestAuthenticationValidator != null) {
|
||||
authorizationCodeRequestAuthenticationProvider.setAuthenticationValidator(
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationValidator()
|
||||
.andThen(this.authorizationCodeRequestAuthenticationValidator));
|
||||
}
|
||||
authenticationProviders.add(authorizationCodeRequestAuthenticationProvider);
|
||||
|
||||
OAuth2AuthorizationConsentAuthenticationProvider authorizationConsentAuthenticationProvider =
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
@@ -27,9 +29,14 @@ import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
@@ -68,15 +75,8 @@ public final class OAuth2AuthorizationServerConfigurer
|
||||
extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer, HttpSecurity> {
|
||||
|
||||
private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = createConfigurers();
|
||||
private final RequestMatcher endpointsMatcher = (request) ->
|
||||
getRequestMatcher(OAuth2AuthorizationServerMetadataEndpointConfigurer.class).matches(request) ||
|
||||
getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) ||
|
||||
getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) ||
|
||||
getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class).matches(request) ||
|
||||
getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class).matches(request) ||
|
||||
getRequestMatcher(OidcConfigurer.class).matches(request) ||
|
||||
this.jwkSetEndpointMatcher.matches(request);
|
||||
private RequestMatcher jwkSetEndpointMatcher;
|
||||
private RequestMatcher endpointsMatcher;
|
||||
|
||||
|
||||
/**
|
||||
* Sets the repository of registered clients.
|
||||
@@ -209,13 +209,18 @@ public final class OAuth2AuthorizationServerConfigurer
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OpenID Connect 1.0 support.
|
||||
* Configures OpenID Connect 1.0 support (disabled by default).
|
||||
*
|
||||
* @param oidcCustomizer the {@link Customizer} providing access to the {@link OidcConfigurer}
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer oidc(Customizer<OidcConfigurer> oidcCustomizer) {
|
||||
oidcCustomizer.customize(getConfigurer(OidcConfigurer.class));
|
||||
OidcConfigurer oidcConfigurer = getConfigurer(OidcConfigurer.class);
|
||||
if (oidcConfigurer == null) {
|
||||
addConfigurer(OidcConfigurer.class, new OidcConfigurer(this::postProcess));
|
||||
oidcConfigurer = getConfigurer(OidcConfigurer.class);
|
||||
}
|
||||
oidcCustomizer.customize(oidcConfigurer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -225,7 +230,9 @@ public final class OAuth2AuthorizationServerConfigurer
|
||||
* @return a {@link RequestMatcher} for the authorization server endpoints
|
||||
*/
|
||||
public RequestMatcher getEndpointsMatcher() {
|
||||
return this.endpointsMatcher;
|
||||
// Return a deferred RequestMatcher
|
||||
// since endpointsMatcher is constructed in init(HttpSecurity).
|
||||
return (request) -> this.endpointsMatcher.matches(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -233,10 +240,33 @@ public final class OAuth2AuthorizationServerConfigurer
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
validateAuthorizationServerSettings(authorizationServerSettings);
|
||||
|
||||
this.jwkSetEndpointMatcher = new AntPathRequestMatcher(
|
||||
authorizationServerSettings.getJwkSetEndpoint(), HttpMethod.GET.name());
|
||||
OidcConfigurer oidcConfigurer = getConfigurer(OidcConfigurer.class);
|
||||
if (oidcConfigurer == null) {
|
||||
// OpenID Connect is disabled.
|
||||
// Add an authentication validator that rejects authentication requests.
|
||||
OAuth2AuthorizationEndpointConfigurer authorizationEndpointConfigurer =
|
||||
getConfigurer(OAuth2AuthorizationEndpointConfigurer.class);
|
||||
authorizationEndpointConfigurer.addAuthorizationCodeRequestAuthenticationValidator((authenticationContext) -> {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
authenticationContext.getAuthentication();
|
||||
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
|
||||
"OpenID Connect 1.0 authentication requests are restricted.",
|
||||
"https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1");
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(
|
||||
error, authorizationCodeRequestAuthentication);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.configurers.values().forEach(configurer -> configurer.init(httpSecurity));
|
||||
List<RequestMatcher> requestMatchers = new ArrayList<>();
|
||||
this.configurers.values().forEach(configurer -> {
|
||||
configurer.init(httpSecurity);
|
||||
requestMatchers.add(configurer.getRequestMatcher());
|
||||
});
|
||||
requestMatchers.add(new AntPathRequestMatcher(
|
||||
authorizationServerSettings.getJwkSetEndpoint(), HttpMethod.GET.name()));
|
||||
this.endpointsMatcher = new OrRequestMatcher(requestMatchers);
|
||||
|
||||
ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = httpSecurity.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptionHandling != null) {
|
||||
@@ -275,7 +305,6 @@ public final class OAuth2AuthorizationServerConfigurer
|
||||
configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
|
||||
configurers.put(OAuth2TokenIntrospectionEndpointConfigurer.class, new OAuth2TokenIntrospectionEndpointConfigurer(this::postProcess));
|
||||
configurers.put(OAuth2TokenRevocationEndpointConfigurer.class, new OAuth2TokenRevocationEndpointConfigurer(this::postProcess));
|
||||
configurers.put(OidcConfigurer.class, new OidcConfigurer(this::postProcess));
|
||||
return configurers;
|
||||
}
|
||||
|
||||
@@ -284,8 +313,13 @@ public final class OAuth2AuthorizationServerConfigurer
|
||||
return (T) this.configurers.get(type);
|
||||
}
|
||||
|
||||
private <T extends AbstractOAuth2Configurer> void addConfigurer(Class<T> configurerType, T configurer) {
|
||||
this.configurers.put(configurerType, configurer);
|
||||
}
|
||||
|
||||
private <T extends AbstractOAuth2Configurer> RequestMatcher getRequestMatcher(Class<T> configurerType) {
|
||||
return getConfigurer(configurerType).getRequestMatcher();
|
||||
T configurer = getConfigurer(configurerType);
|
||||
return configurer != null ? configurer.getRequestMatcher() : null;
|
||||
}
|
||||
|
||||
private static void validateAuthorizationServerSettings(AuthorizationServerSettings authorizationServerSettings) {
|
||||
|
||||
@@ -15,28 +15,53 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcClientRegistrationAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
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.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Configurer for OpenID Connect Dynamic Client Registration 1.0 Endpoint.
|
||||
* Configurer for OpenID Connect 1.0 Dynamic Client Registration Endpoint.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.2.0
|
||||
* @see OidcConfigurer#clientRegistrationEndpoint
|
||||
* @see OidcClientRegistrationEndpointFilter
|
||||
*/
|
||||
public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private final List<AuthenticationConverter> clientRegistrationRequestConverters = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationConverter>> clientRegistrationRequestConvertersConsumer = (clientRegistrationRequestConverters) -> {};
|
||||
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
|
||||
private AuthenticationSuccessHandler clientRegistrationResponseHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
@@ -45,20 +70,108 @@ public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAut
|
||||
super(objectPostProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link AuthenticationConverter} used when attempting to extract a Client Registration Request from {@link HttpServletRequest}
|
||||
* to an instance of {@link OidcClientRegistrationAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @param clientRegistrationRequestConverter an {@link AuthenticationConverter} used when attempting to extract a Client Registration Request from {@link HttpServletRequest}
|
||||
* @return the {@link OidcClientRegistrationEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcClientRegistrationEndpointConfigurer clientRegistrationRequestConverter(
|
||||
AuthenticationConverter clientRegistrationRequestConverter) {
|
||||
Assert.notNull(clientRegistrationRequestConverter, "clientRegistrationRequestConverter cannot be null");
|
||||
this.clientRegistrationRequestConverters.add(clientRegistrationRequestConverter);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@code List} of default
|
||||
* and (optionally) added {@link #clientRegistrationRequestConverter(AuthenticationConverter) AuthenticationConverter}'s
|
||||
* allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}.
|
||||
*
|
||||
* @param clientRegistrationRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s
|
||||
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcClientRegistrationEndpointConfigurer clientRegistrationRequestConverters(
|
||||
Consumer<List<AuthenticationConverter>> clientRegistrationRequestConvertersConsumer) {
|
||||
Assert.notNull(clientRegistrationRequestConvertersConsumer, "clientRegistrationRequestConvertersConsumer cannot be null");
|
||||
this.clientRegistrationRequestConvertersConsumer = clientRegistrationRequestConvertersConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link AuthenticationProvider} used for authenticating an {@link OidcClientRegistrationAuthenticationToken}.
|
||||
*
|
||||
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OidcClientRegistrationAuthenticationToken}
|
||||
* @return the {@link OidcClientRegistrationEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcClientRegistrationEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
|
||||
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
|
||||
this.authenticationProviders.add(authenticationProvider);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@code List} of default
|
||||
* and (optionally) added {@link #authenticationProvider(AuthenticationProvider) AuthenticationProvider}'s
|
||||
* allowing the ability to add, remove, or customize a specific {@link AuthenticationProvider}.
|
||||
*
|
||||
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
|
||||
* @return the {@link OidcClientRegistrationEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcClientRegistrationEndpointConfigurer authenticationProviders(
|
||||
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
|
||||
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
|
||||
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OidcClientRegistrationAuthenticationToken}
|
||||
* and returning the {@link OidcClientRegistration Client Registration Response}.
|
||||
*
|
||||
* @param clientRegistrationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OidcClientRegistrationAuthenticationToken}
|
||||
* @return the {@link OidcClientRegistrationEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcClientRegistrationEndpointConfigurer clientRegistrationResponseHandler(AuthenticationSuccessHandler clientRegistrationResponseHandler) {
|
||||
this.clientRegistrationResponseHandler = clientRegistrationResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* and returning the {@link OAuth2Error Error Response}.
|
||||
*
|
||||
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* @return the {@link OidcClientRegistrationEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcClientRegistrationEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
|
||||
this.errorResponseHandler = errorResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
void init(HttpSecurity httpSecurity) {
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
String clientRegistrationEndpointUri = authorizationServerSettings.getOidcClientRegistrationEndpoint();
|
||||
this.requestMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher(authorizationServerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.POST.name()),
|
||||
new AntPathRequestMatcher(authorizationServerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.GET.name())
|
||||
new AntPathRequestMatcher(clientRegistrationEndpointUri, HttpMethod.POST.name()),
|
||||
new AntPathRequestMatcher(clientRegistrationEndpointUri, HttpMethod.GET.name())
|
||||
);
|
||||
|
||||
OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider =
|
||||
new OidcClientRegistrationAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
|
||||
OAuth2ConfigurerUtils.getTokenGenerator(httpSecurity));
|
||||
httpSecurity.authenticationProvider(postProcess(oidcClientRegistrationAuthenticationProvider));
|
||||
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
|
||||
if (!this.authenticationProviders.isEmpty()) {
|
||||
authenticationProviders.addAll(0, this.authenticationProviders);
|
||||
}
|
||||
this.authenticationProvidersConsumer.accept(authenticationProviders);
|
||||
authenticationProviders.forEach(authenticationProvider ->
|
||||
httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,6 +183,20 @@ public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAut
|
||||
new OidcClientRegistrationEndpointFilter(
|
||||
authenticationManager,
|
||||
authorizationServerSettings.getOidcClientRegistrationEndpoint());
|
||||
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
|
||||
if (!this.clientRegistrationRequestConverters.isEmpty()) {
|
||||
authenticationConverters.addAll(0, this.clientRegistrationRequestConverters);
|
||||
}
|
||||
this.clientRegistrationRequestConvertersConsumer.accept(authenticationConverters);
|
||||
oidcClientRegistrationEndpointFilter.setAuthenticationConverter(
|
||||
new DelegatingAuthenticationConverter(authenticationConverters));
|
||||
if (this.clientRegistrationResponseHandler != null) {
|
||||
oidcClientRegistrationEndpointFilter
|
||||
.setAuthenticationSuccessHandler(this.clientRegistrationResponseHandler);
|
||||
}
|
||||
if (this.errorResponseHandler != null) {
|
||||
oidcClientRegistrationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
|
||||
}
|
||||
httpSecurity.addFilterAfter(postProcess(oidcClientRegistrationEndpointFilter), FilterSecurityInterceptor.class);
|
||||
}
|
||||
|
||||
@@ -78,4 +205,31 @@ public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAut
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
|
||||
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
|
||||
|
||||
authenticationConverters.add(new OidcClientRegistrationAuthenticationConverter());
|
||||
|
||||
return authenticationConverters;
|
||||
}
|
||||
|
||||
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider =
|
||||
new OidcClientRegistrationAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
|
||||
OAuth2ConfigurerUtils.getTokenGenerator(httpSecurity));
|
||||
authenticationProviders.add(oidcClientRegistrationAuthenticationProvider);
|
||||
|
||||
OidcClientConfigurationAuthenticationProvider oidcClientConfigurationAuthenticationProvider =
|
||||
new OidcClientConfigurationAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
|
||||
authenticationProviders.add(oidcClientConfigurationAuthenticationProvider);
|
||||
|
||||
return authenticationProviders;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,13 +15,23 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
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.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
|
||||
@@ -29,21 +39,33 @@ import org.springframework.security.oauth2.server.authorization.oidc.authenticat
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcUserInfoEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
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.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Configurer for OpenID Connect 1.0 UserInfo Endpoint.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.2.1
|
||||
* @see OidcConfigurer#userInfoEndpoint
|
||||
* @see OidcUserInfoEndpointFilter
|
||||
*/
|
||||
public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private final List<AuthenticationConverter> userInfoRequestConverters = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationConverter>> userInfoRequestConvertersConsumer = (userInfoRequestConverters) -> {};
|
||||
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
|
||||
private AuthenticationSuccessHandler userInfoResponseHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
private Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper;
|
||||
|
||||
/**
|
||||
@@ -53,6 +75,91 @@ public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configur
|
||||
super(objectPostProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link AuthenticationConverter} used when attempting to extract an UserInfo Request from {@link HttpServletRequest}
|
||||
* to an instance of {@link OidcUserInfoAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @param userInfoRequestConverter an {@link AuthenticationConverter} used when attempting to extract an UserInfo Request from {@link HttpServletRequest}
|
||||
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcUserInfoEndpointConfigurer userInfoRequestConverter(AuthenticationConverter userInfoRequestConverter) {
|
||||
Assert.notNull(userInfoRequestConverter, "userInfoRequestConverter cannot be null");
|
||||
this.userInfoRequestConverters.add(userInfoRequestConverter);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@code List} of default
|
||||
* and (optionally) added {@link #userInfoRequestConverter(AuthenticationConverter) AuthenticationConverter}'s
|
||||
* allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}.
|
||||
*
|
||||
* @param userInfoRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s
|
||||
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcUserInfoEndpointConfigurer userInfoRequestConverters(
|
||||
Consumer<List<AuthenticationConverter>> userInfoRequestConvertersConsumer) {
|
||||
Assert.notNull(userInfoRequestConvertersConsumer, "userInfoRequestConvertersConsumer cannot be null");
|
||||
this.userInfoRequestConvertersConsumer = userInfoRequestConvertersConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link AuthenticationProvider} used for authenticating an {@link OidcUserInfoAuthenticationToken}.
|
||||
*
|
||||
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OidcUserInfoAuthenticationToken}
|
||||
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcUserInfoEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
|
||||
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
|
||||
this.authenticationProviders.add(authenticationProvider);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@code List} of default
|
||||
* and (optionally) added {@link #authenticationProvider(AuthenticationProvider) AuthenticationProvider}'s
|
||||
* allowing the ability to add, remove, or customize a specific {@link AuthenticationProvider}.
|
||||
*
|
||||
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
|
||||
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcUserInfoEndpointConfigurer authenticationProviders(
|
||||
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
|
||||
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
|
||||
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OidcUserInfoAuthenticationToken}
|
||||
* and returning the {@link OidcUserInfo UserInfo Response}.
|
||||
*
|
||||
* @param userInfoResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OidcUserInfoAuthenticationToken}
|
||||
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcUserInfoEndpointConfigurer userInfoResponseHandler(AuthenticationSuccessHandler userInfoResponseHandler) {
|
||||
this.userInfoResponseHandler = userInfoResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* and returning the {@link OAuth2Error Error Response}.
|
||||
*
|
||||
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcUserInfoEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
|
||||
this.errorResponseHandler = errorResponseHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Function} used to extract claims from {@link OidcUserInfoAuthenticationContext}
|
||||
* to an instance of {@link OidcUserInfo} for the UserInfo response.
|
||||
@@ -69,7 +176,8 @@ public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configur
|
||||
* @param userInfoMapper the {@link Function} used to extract claims from {@link OidcUserInfoAuthenticationContext} to an instance of {@link OidcUserInfo}
|
||||
* @return the {@link OidcUserInfoEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OidcUserInfoEndpointConfigurer userInfoMapper(Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper) {
|
||||
public OidcUserInfoEndpointConfigurer userInfoMapper(
|
||||
Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper) {
|
||||
this.userInfoMapper = userInfoMapper;
|
||||
return this;
|
||||
}
|
||||
@@ -82,13 +190,13 @@ public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configur
|
||||
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.GET.name()),
|
||||
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.POST.name()));
|
||||
|
||||
OidcUserInfoAuthenticationProvider oidcUserInfoAuthenticationProvider =
|
||||
new OidcUserInfoAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
|
||||
if (this.userInfoMapper != null) {
|
||||
oidcUserInfoAuthenticationProvider.setUserInfoMapper(this.userInfoMapper);
|
||||
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
|
||||
if (!this.authenticationProviders.isEmpty()) {
|
||||
authenticationProviders.addAll(0, this.authenticationProviders);
|
||||
}
|
||||
httpSecurity.authenticationProvider(postProcess(oidcUserInfoAuthenticationProvider));
|
||||
this.authenticationProvidersConsumer.accept(authenticationProviders);
|
||||
authenticationProviders.forEach(authenticationProvider ->
|
||||
httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -100,6 +208,19 @@ public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configur
|
||||
new OidcUserInfoEndpointFilter(
|
||||
authenticationManager,
|
||||
authorizationServerSettings.getOidcUserInfoEndpoint());
|
||||
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
|
||||
if (!this.userInfoRequestConverters.isEmpty()) {
|
||||
authenticationConverters.addAll(0, this.userInfoRequestConverters);
|
||||
}
|
||||
this.userInfoRequestConvertersConsumer.accept(authenticationConverters);
|
||||
oidcUserInfoEndpointFilter.setAuthenticationConverter(
|
||||
new DelegatingAuthenticationConverter(authenticationConverters));
|
||||
if (this.userInfoResponseHandler != null) {
|
||||
oidcUserInfoEndpointFilter.setAuthenticationSuccessHandler(this.userInfoResponseHandler);
|
||||
}
|
||||
if (this.errorResponseHandler != null) {
|
||||
oidcUserInfoEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
|
||||
}
|
||||
httpSecurity.addFilterAfter(postProcess(oidcUserInfoEndpointFilter), FilterSecurityInterceptor.class);
|
||||
}
|
||||
|
||||
@@ -108,4 +229,31 @@ public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configur
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
|
||||
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
|
||||
|
||||
authenticationConverters.add(
|
||||
(request) -> {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return new OidcUserInfoAuthenticationToken(authentication);
|
||||
}
|
||||
);
|
||||
|
||||
return authenticationConverters;
|
||||
}
|
||||
|
||||
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
OidcUserInfoAuthenticationProvider oidcUserInfoAuthenticationProvider =
|
||||
new OidcUserInfoAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
|
||||
if (this.userInfoMapper != null) {
|
||||
oidcUserInfoAuthenticationProvider.setUserInfoMapper(this.userInfoMapper);
|
||||
}
|
||||
authenticationProviders.add(oidcUserInfoAuthenticationProvider);
|
||||
|
||||
return authenticationProviders;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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.oidc.authentication;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
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.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for OpenID Connect 1.0 Dynamic Client Configuration Endpoint.
|
||||
*
|
||||
* @author Ovidiu Popa
|
||||
* @author Joe Grandja
|
||||
* @author Rafal Lewczuk
|
||||
* @since 0.4.0
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OidcClientRegistrationAuthenticationToken
|
||||
* @see OidcClientRegistrationAuthenticationProvider
|
||||
* @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientConfigurationEndpoint">4. Client Configuration Endpoint</a>
|
||||
*/
|
||||
public final class OidcClientConfigurationAuthenticationProvider implements AuthenticationProvider {
|
||||
static final String DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE = "client.read";
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final Converter<RegisteredClient, OidcClientRegistration> clientRegistrationConverter;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OidcClientConfigurationAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param registeredClientRepository the repository of registered clients
|
||||
* @param authorizationService the authorization service
|
||||
*/
|
||||
public OidcClientConfigurationAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
|
||||
OAuth2AuthorizationService authorizationService) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.authorizationService = authorizationService;
|
||||
this.clientRegistrationConverter = new RegisteredClientOidcClientRegistrationConverter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication =
|
||||
(OidcClientRegistrationAuthenticationToken) authentication;
|
||||
|
||||
if (!StringUtils.hasText(clientRegistrationAuthentication.getClientId())) {
|
||||
// This is not a Client Configuration Request.
|
||||
// Return null to allow OidcClientRegistrationAuthenticationProvider to handle it.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate the "registration" access token
|
||||
AbstractOAuth2TokenAuthenticationToken<?> accessTokenAuthentication = null;
|
||||
if (AbstractOAuth2TokenAuthenticationToken.class.isAssignableFrom(clientRegistrationAuthentication.getPrincipal().getClass())) {
|
||||
accessTokenAuthentication = (AbstractOAuth2TokenAuthenticationToken<?>) clientRegistrationAuthentication.getPrincipal();
|
||||
}
|
||||
if (accessTokenAuthentication == null || !accessTokenAuthentication.isAuthenticated()) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
|
||||
String accessTokenValue = accessTokenAuthentication.getToken().getTokenValue();
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
accessTokenValue, OAuth2TokenType.ACCESS_TOKEN);
|
||||
if (authorization == null) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
|
||||
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
|
||||
if (!authorizedAccessToken.isActive()) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
checkScope(authorizedAccessToken, Collections.singleton(DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE));
|
||||
|
||||
return findRegistration(clientRegistrationAuthentication, authorization);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OidcClientRegistrationAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private OidcClientRegistrationAuthenticationToken findRegistration(OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication,
|
||||
OAuth2Authorization authorization) {
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
clientRegistrationAuthentication.getClientId());
|
||||
if (registeredClient == null) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
OidcClientRegistration clientRegistration = this.clientRegistrationConverter.convert(registeredClient);
|
||||
|
||||
return new OidcClientRegistrationAuthenticationToken(
|
||||
(Authentication) clientRegistrationAuthentication.getPrincipal(), clientRegistration);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void checkScope(OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken, Set<String> requiredScope) {
|
||||
Collection<String> authorizedScope = Collections.emptySet();
|
||||
if (authorizedAccessToken.getClaims().containsKey(OAuth2ParameterNames.SCOPE)) {
|
||||
authorizedScope = (Collection<String>) authorizedAccessToken.getClaims().get(OAuth2ParameterNames.SCOPE);
|
||||
}
|
||||
if (!authorizedScope.containsAll(requiredScope)) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
|
||||
} else if (authorizedScope.size() != requiredScope.size()) {
|
||||
// Restrict the access token to only contain the required scope
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,9 +23,11 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
@@ -49,7 +51,6 @@ import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientMetadataClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||
@@ -62,10 +63,9 @@ import org.springframework.security.oauth2.server.resource.authentication.Abstra
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for OpenID Connect 1.0 Dynamic Client Registration (and Configuration) Endpoint.
|
||||
* An {@link AuthenticationProvider} implementation for OpenID Connect 1.0 Dynamic Client Registration Endpoint.
|
||||
*
|
||||
* @author Ovidiu Popa
|
||||
* @author Joe Grandja
|
||||
@@ -74,20 +74,18 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OAuth2TokenGenerator
|
||||
* @see OidcClientRegistrationAuthenticationToken
|
||||
* @see OidcClientConfigurationAuthenticationProvider
|
||||
* @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration">3. Client Registration Endpoint</a>
|
||||
* @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientConfigurationEndpoint">4. Client Configuration Endpoint</a>
|
||||
*/
|
||||
public final class OidcClientRegistrationAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError";
|
||||
private static final StringKeyGenerator CLIENT_ID_GENERATOR = new Base64StringKeyGenerator(
|
||||
Base64.getUrlEncoder().withoutPadding(), 32);
|
||||
private static final StringKeyGenerator CLIENT_SECRET_GENERATOR = new Base64StringKeyGenerator(
|
||||
Base64.getUrlEncoder().withoutPadding(), 48);
|
||||
private static final String DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE = "client.create";
|
||||
private static final String DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE = "client.read";
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
private final Converter<RegisteredClient, OidcClientRegistration> clientRegistrationConverter;
|
||||
private Converter<OidcClientRegistration, RegisteredClient> registeredClientConverter;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OidcClientRegistrationAuthenticationProvider} using the provided parameters.
|
||||
@@ -105,6 +103,8 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.authorizationService = authorizationService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
this.clientRegistrationConverter = new RegisteredClientOidcClientRegistrationConverter();
|
||||
this.registeredClientConverter = new OidcClientRegistrationRegisteredClientConverter();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -112,7 +112,13 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication =
|
||||
(OidcClientRegistrationAuthenticationToken) authentication;
|
||||
|
||||
// Validate the "initial" or "registration" access token
|
||||
if (clientRegistrationAuthentication.getClientRegistration() == null) {
|
||||
// This is not a Client Registration Request.
|
||||
// Return null to allow OidcClientConfigurationAuthenticationProvider to handle it.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate the "initial" access token
|
||||
AbstractOAuth2TokenAuthenticationToken<?> accessTokenAuthentication = null;
|
||||
if (AbstractOAuth2TokenAuthenticationToken.class.isAssignableFrom(clientRegistrationAuthentication.getPrincipal().getClass())) {
|
||||
accessTokenAuthentication = (AbstractOAuth2TokenAuthenticationToken<?>) clientRegistrationAuthentication.getPrincipal();
|
||||
@@ -122,7 +128,6 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
}
|
||||
|
||||
String accessTokenValue = accessTokenAuthentication.getToken().getTokenValue();
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
accessTokenValue, OAuth2TokenType.ACCESS_TOKEN);
|
||||
if (authorization == null) {
|
||||
@@ -133,10 +138,9 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
if (!authorizedAccessToken.isActive()) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
checkScope(authorizedAccessToken, Collections.singleton(DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE));
|
||||
|
||||
return clientRegistrationAuthentication.getClientRegistration() != null ?
|
||||
registerClient(clientRegistrationAuthentication, authorization) :
|
||||
findRegistration(clientRegistrationAuthentication, authorization);
|
||||
return registerClient(clientRegistrationAuthentication, authorization);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -144,34 +148,20 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
return OidcClientRegistrationAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private OidcClientRegistrationAuthenticationToken findRegistration(OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication,
|
||||
OAuth2Authorization authorization) {
|
||||
|
||||
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
|
||||
checkScopeForConfiguration(authorizedAccessToken);
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
clientRegistrationAuthentication.getClientId());
|
||||
if (registeredClient == null) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
OidcClientRegistration clientRegistration = buildRegistration(registeredClient).build();
|
||||
|
||||
return new OidcClientRegistrationAuthenticationToken(
|
||||
(Authentication) clientRegistrationAuthentication.getPrincipal(), clientRegistration);
|
||||
/**
|
||||
* Sets the {@link Converter} used for converting an {@link OidcClientRegistration} to a {@link RegisteredClient}.
|
||||
*
|
||||
* @param registeredClientConverter the {@link Converter} used for converting an {@link OidcClientRegistration} to a {@link RegisteredClient}
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public void setRegisteredClientConverter(Converter<OidcClientRegistration, RegisteredClient> registeredClientConverter) {
|
||||
Assert.notNull(registeredClientConverter, "registeredClientConverter cannot be null");
|
||||
this.registeredClientConverter = registeredClientConverter;
|
||||
}
|
||||
|
||||
private OidcClientRegistrationAuthenticationToken registerClient(OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication,
|
||||
OAuth2Authorization authorization) {
|
||||
|
||||
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
|
||||
checkScopeForRegistration(authorizedAccessToken);
|
||||
|
||||
if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getRedirectUris())) {
|
||||
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI, OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
@@ -180,19 +170,20 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
throwInvalidClientRegistration("invalid_client_metadata", OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
|
||||
}
|
||||
|
||||
RegisteredClient registeredClient = createClient(clientRegistrationAuthentication.getClientRegistration());
|
||||
RegisteredClient registeredClient = this.registeredClientConverter.convert(clientRegistrationAuthentication.getClientRegistration());
|
||||
this.registeredClientRepository.save(registeredClient);
|
||||
|
||||
OAuth2Authorization registeredClientAuthorization = registerAccessToken(registeredClient);
|
||||
|
||||
// Invalidate the "initial" access token as it can only be used once
|
||||
authorization = OidcAuthenticationProviderUtils.invalidate(authorization, authorizedAccessToken.getToken());
|
||||
authorization = OidcAuthenticationProviderUtils.invalidate(authorization, authorization.getAccessToken().getToken());
|
||||
if (authorization.getRefreshToken() != null) {
|
||||
authorization = OidcAuthenticationProviderUtils.invalidate(authorization, authorization.getRefreshToken().getToken());
|
||||
}
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
OidcClientRegistration clientRegistration = buildRegistration(registeredClient)
|
||||
Map<String, Object> clientRegistrationClaims = this.clientRegistrationConverter.convert(registeredClient).getClaims();
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.withClaims(clientRegistrationClaims)
|
||||
.registrationAccessToken(registeredClientAuthorization.getAccessToken().getToken().getTokenValue())
|
||||
.build();
|
||||
|
||||
@@ -205,7 +196,7 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
registeredClient.getClientAuthenticationMethods().iterator().next(), registeredClient.getClientSecret());
|
||||
|
||||
Set<String> authorizedScopes = new HashSet<>();
|
||||
authorizedScopes.add(DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE);
|
||||
authorizedScopes.add(OidcClientConfigurationAuthenticationProvider.DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE);
|
||||
authorizedScopes = Collections.unmodifiableSet(authorizedScopes);
|
||||
|
||||
// @formatter:off
|
||||
@@ -249,66 +240,6 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
return authorization;
|
||||
}
|
||||
|
||||
private OidcClientRegistration.Builder buildRegistration(RegisteredClient registeredClient) {
|
||||
// @formatter:off
|
||||
OidcClientRegistration.Builder builder = OidcClientRegistration.builder()
|
||||
.clientId(registeredClient.getClientId())
|
||||
.clientIdIssuedAt(registeredClient.getClientIdIssuedAt())
|
||||
.clientName(registeredClient.getClientName());
|
||||
|
||||
if (registeredClient.getClientSecret() != null) {
|
||||
builder.clientSecret(registeredClient.getClientSecret());
|
||||
}
|
||||
|
||||
builder.redirectUris(redirectUris ->
|
||||
redirectUris.addAll(registeredClient.getRedirectUris()));
|
||||
|
||||
builder.grantTypes(grantTypes ->
|
||||
registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType ->
|
||||
grantTypes.add(authorizationGrantType.getValue())));
|
||||
|
||||
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
|
||||
builder.responseType(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
}
|
||||
|
||||
if (!CollectionUtils.isEmpty(registeredClient.getScopes())) {
|
||||
builder.scopes(scopes ->
|
||||
scopes.addAll(registeredClient.getScopes()));
|
||||
}
|
||||
|
||||
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
|
||||
String registrationClientUri = UriComponentsBuilder.fromUriString(authorizationServerContext.getIssuer())
|
||||
.path(authorizationServerContext.getAuthorizationServerSettings().getOidcClientRegistrationEndpoint())
|
||||
.queryParam(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
||||
.toUriString();
|
||||
|
||||
builder
|
||||
.tokenEndpointAuthenticationMethod(registeredClient.getClientAuthenticationMethods().iterator().next().getValue())
|
||||
.idTokenSignedResponseAlgorithm(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName())
|
||||
.registrationClientUrl(registrationClientUri);
|
||||
|
||||
ClientSettings clientSettings = registeredClient.getClientSettings();
|
||||
|
||||
if (clientSettings.getJwkSetUrl() != null) {
|
||||
builder.jwkSetUrl(clientSettings.getJwkSetUrl());
|
||||
}
|
||||
|
||||
if (clientSettings.getTokenEndpointAuthenticationSigningAlgorithm() != null) {
|
||||
builder.tokenEndpointAuthenticationSigningAlgorithm(clientSettings.getTokenEndpointAuthenticationSigningAlgorithm().getName());
|
||||
}
|
||||
|
||||
return builder;
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static void checkScopeForRegistration(OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken) {
|
||||
checkScope(authorizedAccessToken, Collections.singleton(DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE));
|
||||
}
|
||||
|
||||
private static void checkScopeForConfiguration(OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken) {
|
||||
checkScope(authorizedAccessToken, Collections.singleton(DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void checkScope(OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken, Set<String> requiredScope) {
|
||||
Collection<String> authorizedScope = Collections.emptySet();
|
||||
@@ -366,78 +297,6 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
}
|
||||
}
|
||||
|
||||
private static RegisteredClient createClient(OidcClientRegistration clientRegistration) {
|
||||
// @formatter:off
|
||||
RegisteredClient.Builder builder = RegisteredClient.withId(UUID.randomUUID().toString())
|
||||
.clientId(CLIENT_ID_GENERATOR.generateKey())
|
||||
.clientIdIssuedAt(Instant.now())
|
||||
.clientName(clientRegistration.getClientName());
|
||||
|
||||
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
|
||||
builder
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
|
||||
.clientSecret(CLIENT_SECRET_GENERATOR.generateKey());
|
||||
} else if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
|
||||
builder
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
|
||||
.clientSecret(CLIENT_SECRET_GENERATOR.generateKey());
|
||||
} else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
|
||||
builder.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
|
||||
} else {
|
||||
builder
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.clientSecret(CLIENT_SECRET_GENERATOR.generateKey());
|
||||
}
|
||||
|
||||
builder.redirectUris(redirectUris ->
|
||||
redirectUris.addAll(clientRegistration.getRedirectUris()));
|
||||
|
||||
if (!CollectionUtils.isEmpty(clientRegistration.getGrantTypes())) {
|
||||
builder.authorizationGrantTypes(authorizationGrantTypes ->
|
||||
clientRegistration.getGrantTypes().forEach(grantType ->
|
||||
authorizationGrantTypes.add(new AuthorizationGrantType(grantType))));
|
||||
} else {
|
||||
builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
}
|
||||
if (CollectionUtils.isEmpty(clientRegistration.getResponseTypes()) ||
|
||||
clientRegistration.getResponseTypes().contains(OAuth2AuthorizationResponseType.CODE.getValue())) {
|
||||
builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
}
|
||||
|
||||
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
|
||||
builder.scopes(scopes ->
|
||||
scopes.addAll(clientRegistration.getScopes()));
|
||||
}
|
||||
|
||||
ClientSettings.Builder clientSettingsBuilder = ClientSettings.builder()
|
||||
.requireProofKey(true)
|
||||
.requireAuthorizationConsent(true);
|
||||
|
||||
if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
|
||||
MacAlgorithm macAlgorithm = MacAlgorithm.from(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm());
|
||||
if (macAlgorithm == null) {
|
||||
macAlgorithm = MacAlgorithm.HS256;
|
||||
}
|
||||
clientSettingsBuilder.tokenEndpointAuthenticationSigningAlgorithm(macAlgorithm);
|
||||
} else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
|
||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm());
|
||||
if (signatureAlgorithm == null) {
|
||||
signatureAlgorithm = SignatureAlgorithm.RS256;
|
||||
}
|
||||
clientSettingsBuilder.tokenEndpointAuthenticationSigningAlgorithm(signatureAlgorithm);
|
||||
clientSettingsBuilder.jwkSetUrl(clientRegistration.getJwkSetUrl().toString());
|
||||
}
|
||||
|
||||
builder
|
||||
.clientSettings(clientSettingsBuilder.build())
|
||||
.tokenSettings(TokenSettings.builder()
|
||||
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
|
||||
.build());
|
||||
|
||||
return builder.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
errorCode,
|
||||
@@ -446,4 +305,84 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
private static final class OidcClientRegistrationRegisteredClientConverter implements Converter<OidcClientRegistration, RegisteredClient> {
|
||||
private static final StringKeyGenerator CLIENT_ID_GENERATOR = new Base64StringKeyGenerator(
|
||||
Base64.getUrlEncoder().withoutPadding(), 32);
|
||||
private static final StringKeyGenerator CLIENT_SECRET_GENERATOR = new Base64StringKeyGenerator(
|
||||
Base64.getUrlEncoder().withoutPadding(), 48);
|
||||
|
||||
@Override
|
||||
public RegisteredClient convert(OidcClientRegistration clientRegistration) {
|
||||
// @formatter:off
|
||||
RegisteredClient.Builder builder = RegisteredClient.withId(UUID.randomUUID().toString())
|
||||
.clientId(CLIENT_ID_GENERATOR.generateKey())
|
||||
.clientIdIssuedAt(Instant.now())
|
||||
.clientName(clientRegistration.getClientName());
|
||||
|
||||
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
|
||||
builder
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
|
||||
.clientSecret(CLIENT_SECRET_GENERATOR.generateKey());
|
||||
} else if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
|
||||
builder
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
|
||||
.clientSecret(CLIENT_SECRET_GENERATOR.generateKey());
|
||||
} else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
|
||||
builder.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
|
||||
} else {
|
||||
builder
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.clientSecret(CLIENT_SECRET_GENERATOR.generateKey());
|
||||
}
|
||||
|
||||
builder.redirectUris(redirectUris ->
|
||||
redirectUris.addAll(clientRegistration.getRedirectUris()));
|
||||
|
||||
if (!CollectionUtils.isEmpty(clientRegistration.getGrantTypes())) {
|
||||
builder.authorizationGrantTypes(authorizationGrantTypes ->
|
||||
clientRegistration.getGrantTypes().forEach(grantType ->
|
||||
authorizationGrantTypes.add(new AuthorizationGrantType(grantType))));
|
||||
} else {
|
||||
builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
}
|
||||
if (CollectionUtils.isEmpty(clientRegistration.getResponseTypes()) ||
|
||||
clientRegistration.getResponseTypes().contains(OAuth2AuthorizationResponseType.CODE.getValue())) {
|
||||
builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
}
|
||||
|
||||
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
|
||||
builder.scopes(scopes ->
|
||||
scopes.addAll(clientRegistration.getScopes()));
|
||||
}
|
||||
|
||||
ClientSettings.Builder clientSettingsBuilder = ClientSettings.builder()
|
||||
.requireProofKey(true)
|
||||
.requireAuthorizationConsent(true);
|
||||
|
||||
if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
|
||||
MacAlgorithm macAlgorithm = MacAlgorithm.from(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm());
|
||||
if (macAlgorithm == null) {
|
||||
macAlgorithm = MacAlgorithm.HS256;
|
||||
}
|
||||
clientSettingsBuilder.tokenEndpointAuthenticationSigningAlgorithm(macAlgorithm);
|
||||
} else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
|
||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm());
|
||||
if (signatureAlgorithm == null) {
|
||||
signatureAlgorithm = SignatureAlgorithm.RS256;
|
||||
}
|
||||
clientSettingsBuilder.tokenEndpointAuthenticationSigningAlgorithm(signatureAlgorithm);
|
||||
clientSettingsBuilder.jwkSetUrl(clientRegistration.getJwkSetUrl().toString());
|
||||
}
|
||||
|
||||
builder
|
||||
.clientSettings(clientSettingsBuilder.build())
|
||||
.tokenSettings(TokenSettings.builder()
|
||||
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
|
||||
.build());
|
||||
|
||||
return builder.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.springframework.util.Assert;
|
||||
* @see AbstractAuthenticationToken
|
||||
* @see OidcClientRegistration
|
||||
* @see OidcClientRegistrationAuthenticationProvider
|
||||
* @see OidcClientConfigurationAuthenticationProvider
|
||||
*/
|
||||
public class OidcClientRegistrationAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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.oidc.authentication;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
*/
|
||||
final class RegisteredClientOidcClientRegistrationConverter implements Converter<RegisteredClient, OidcClientRegistration> {
|
||||
|
||||
@Override
|
||||
public OidcClientRegistration convert(RegisteredClient registeredClient) {
|
||||
// @formatter:off
|
||||
OidcClientRegistration.Builder builder = OidcClientRegistration.builder()
|
||||
.clientId(registeredClient.getClientId())
|
||||
.clientIdIssuedAt(registeredClient.getClientIdIssuedAt())
|
||||
.clientName(registeredClient.getClientName());
|
||||
|
||||
if (registeredClient.getClientSecret() != null) {
|
||||
builder.clientSecret(registeredClient.getClientSecret());
|
||||
}
|
||||
|
||||
builder.redirectUris(redirectUris ->
|
||||
redirectUris.addAll(registeredClient.getRedirectUris()));
|
||||
|
||||
builder.grantTypes(grantTypes ->
|
||||
registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType ->
|
||||
grantTypes.add(authorizationGrantType.getValue())));
|
||||
|
||||
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
|
||||
builder.responseType(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
}
|
||||
|
||||
if (!CollectionUtils.isEmpty(registeredClient.getScopes())) {
|
||||
builder.scopes(scopes ->
|
||||
scopes.addAll(registeredClient.getScopes()));
|
||||
}
|
||||
|
||||
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
|
||||
String registrationClientUri = UriComponentsBuilder.fromUriString(authorizationServerContext.getIssuer())
|
||||
.path(authorizationServerContext.getAuthorizationServerSettings().getOidcClientRegistrationEndpoint())
|
||||
.queryParam(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
||||
.toUriString();
|
||||
|
||||
builder
|
||||
.tokenEndpointAuthenticationMethod(registeredClient.getClientAuthenticationMethods().iterator().next().getValue())
|
||||
.idTokenSignedResponseAlgorithm(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName())
|
||||
.registrationClientUrl(registrationClientUri);
|
||||
|
||||
ClientSettings clientSettings = registeredClient.getClientSettings();
|
||||
|
||||
if (clientSettings.getJwkSetUrl() != null) {
|
||||
builder.jwkSetUrl(clientSettings.getJwkSetUrl());
|
||||
}
|
||||
|
||||
if (clientSettings.getTokenEndpointAuthenticationSigningAlgorithm() != null) {
|
||||
builder.tokenEndpointAuthenticationSigningAlgorithm(clientSettings.getTokenEndpointAuthenticationSigningAlgorithm().getName());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,10 +25,10 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
@@ -36,8 +36,14 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcClientRegistrationAuthenticationConverter;
|
||||
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.AndRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
@@ -47,12 +53,16 @@ import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* A {@code Filter} that processes OpenID Connect Dynamic Client Registration (and Configuration) 1.0 Requests.
|
||||
* A {@code Filter} that processes OpenID Connect 1.0 Dynamic Client Registration (and Client Read) Requests.
|
||||
*
|
||||
* @author Ovidiu Popa
|
||||
* @author Joe Grandja
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.1.1
|
||||
* @see OidcClientRegistration
|
||||
* @see OidcClientRegistrationAuthenticationConverter
|
||||
* @see OidcClientRegistrationAuthenticationProvider
|
||||
* @see OidcClientConfigurationAuthenticationProvider
|
||||
* @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration">3. Client Registration Endpoint</a>
|
||||
* @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientConfigurationEndpoint">4. Client Configuration Endpoint</a>
|
||||
*/
|
||||
@@ -68,6 +78,9 @@ public final class OidcClientRegistrationEndpointFilter extends OncePerRequestFi
|
||||
new OidcClientRegistrationHttpMessageConverter();
|
||||
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
|
||||
new OAuth2ErrorHttpMessageConverter();
|
||||
private AuthenticationConverter authenticationConverter = new OidcClientRegistrationAuthenticationConverter();
|
||||
private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendClientRegistrationResponse;
|
||||
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OidcClientRegistrationEndpointFilter} using the provided parameters.
|
||||
@@ -92,11 +105,11 @@ public final class OidcClientRegistrationEndpointFilter extends OncePerRequestFi
|
||||
this.clientRegistrationEndpointMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher(
|
||||
clientRegistrationEndpointUri, HttpMethod.POST.name()),
|
||||
createConfigureClientMatcher(clientRegistrationEndpointUri));
|
||||
createClientConfigurationMatcher(clientRegistrationEndpointUri));
|
||||
}
|
||||
|
||||
private static RequestMatcher createConfigureClientMatcher(String clientRegistrationEndpointUri) {
|
||||
RequestMatcher configureClientGetMatcher = new AntPathRequestMatcher(
|
||||
private static RequestMatcher createClientConfigurationMatcher(String clientRegistrationEndpointUri) {
|
||||
RequestMatcher clientConfigurationGetMatcher = new AntPathRequestMatcher(
|
||||
clientRegistrationEndpointUri, HttpMethod.GET.name());
|
||||
|
||||
RequestMatcher clientIdMatcher = request -> {
|
||||
@@ -104,7 +117,7 @@ public final class OidcClientRegistrationEndpointFilter extends OncePerRequestFi
|
||||
return StringUtils.hasText(clientId);
|
||||
};
|
||||
|
||||
return new AndRequestMatcher(configureClientGetMatcher, clientIdMatcher);
|
||||
return new AndRequestMatcher(clientConfigurationGetMatcher, clientIdMatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,57 +130,78 @@ public final class OidcClientRegistrationEndpointFilter extends OncePerRequestFi
|
||||
}
|
||||
|
||||
try {
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = convert(request);
|
||||
Authentication clientRegistrationAuthentication = this.authenticationConverter.convert(request);
|
||||
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthenticationResult =
|
||||
(OidcClientRegistrationAuthenticationToken) this.authenticationManager.authenticate(clientRegistrationAuthentication);
|
||||
|
||||
HttpStatus httpStatus = HttpStatus.OK;
|
||||
if (clientRegistrationAuthentication.getClientRegistration() != null) {
|
||||
httpStatus = HttpStatus.CREATED;
|
||||
}
|
||||
|
||||
sendClientRegistrationResponse(response, httpStatus, clientRegistrationAuthenticationResult.getClientRegistration());
|
||||
Authentication clientRegistrationAuthenticationResult =
|
||||
this.authenticationManager.authenticate(clientRegistrationAuthentication);
|
||||
|
||||
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, clientRegistrationAuthenticationResult);
|
||||
} catch (OAuth2AuthenticationException ex) {
|
||||
sendErrorResponse(response, ex.getError());
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
|
||||
} catch (Exception ex) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
"OpenID Client Registration Error: " + ex.getMessage(),
|
||||
"OpenID Connect 1.0 Client Registration Error: " + ex.getMessage(),
|
||||
"https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError");
|
||||
sendErrorResponse(response, error);
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
|
||||
new OAuth2AuthenticationException(error));
|
||||
} finally {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
|
||||
private OidcClientRegistrationAuthenticationToken convert(HttpServletRequest request) throws Exception {
|
||||
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if ("POST".equals(request.getMethod())) {
|
||||
OidcClientRegistration clientRegistration = this.clientRegistrationHttpMessageConverter.read(
|
||||
OidcClientRegistration.class, new ServletServerHttpRequest(request));
|
||||
return new OidcClientRegistrationAuthenticationToken(principal, clientRegistration);
|
||||
}
|
||||
|
||||
// client_id (REQUIRED)
|
||||
String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID);
|
||||
String[] clientIdParameters = request.getParameterValues(OAuth2ParameterNames.CLIENT_ID);
|
||||
if (!StringUtils.hasText(clientId) || clientIdParameters.length != 1) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
return new OidcClientRegistrationAuthenticationToken(principal, clientId);
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract a Client Registration Request from {@link HttpServletRequest}
|
||||
* to an instance of {@link OidcClientRegistrationAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @param authenticationConverter an {@link AuthenticationConverter} used when attempting to extract a Client Registration Request from {@link HttpServletRequest}
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
}
|
||||
|
||||
private void sendClientRegistrationResponse(HttpServletResponse response, HttpStatus httpStatus, OidcClientRegistration clientRegistration) throws IOException {
|
||||
/**
|
||||
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OidcClientRegistrationAuthenticationToken}
|
||||
* and returning the {@link OidcClientRegistration Client Registration Response}.
|
||||
*
|
||||
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OidcClientRegistrationAuthenticationToken}
|
||||
* @see 0.4.0
|
||||
*/
|
||||
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
|
||||
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
|
||||
this.authenticationSuccessHandler = authenticationSuccessHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* and returning the {@link OAuth2Error Error Response}.
|
||||
*
|
||||
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
|
||||
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
|
||||
this.authenticationFailureHandler = authenticationFailureHandler;
|
||||
}
|
||||
|
||||
private void sendClientRegistrationResponse(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) throws IOException {
|
||||
OidcClientRegistration clientRegistration = ((OidcClientRegistrationAuthenticationToken) authentication)
|
||||
.getClientRegistration();
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
httpResponse.setStatusCode(httpStatus);
|
||||
if (HttpMethod.POST.name().equals(request.getMethod())) {
|
||||
httpResponse.setStatusCode(HttpStatus.CREATED);
|
||||
} else {
|
||||
httpResponse.setStatusCode(HttpStatus.OK);
|
||||
}
|
||||
this.clientRegistrationHttpMessageConverter.write(clientRegistration, null, httpResponse);
|
||||
}
|
||||
|
||||
private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
|
||||
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authenticationException) throws IOException {
|
||||
OAuth2Error error = ((OAuth2AuthenticationException) authenticationException).getError();
|
||||
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
|
||||
if (OAuth2ErrorCodes.INVALID_TOKEN.equals(error.getErrorCode())) {
|
||||
httpStatus = HttpStatus.UNAUTHORIZED;
|
||||
|
||||
@@ -28,14 +28,19 @@ import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
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.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcUserInfoHttpMessageConverter;
|
||||
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.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
@@ -47,8 +52,10 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
||||
*
|
||||
* @author Ido Salomon
|
||||
* @author Steve Riesenberg
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 0.2.1
|
||||
* @see OidcUserInfo
|
||||
* @see OidcUserInfoAuthenticationProvider
|
||||
* @see <a href="https://openid.net/specs/openid-connect-core-1_0.html#UserInfo">5.3. UserInfo Endpoint</a>
|
||||
*/
|
||||
public final class OidcUserInfoEndpointFilter extends OncePerRequestFilter {
|
||||
@@ -60,11 +67,13 @@ public final class OidcUserInfoEndpointFilter extends OncePerRequestFilter {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final RequestMatcher userInfoEndpointMatcher;
|
||||
|
||||
private final HttpMessageConverter<OidcUserInfo> userInfoHttpMessageConverter =
|
||||
new OidcUserInfoHttpMessageConverter();
|
||||
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
|
||||
new OAuth2ErrorHttpMessageConverter();
|
||||
private AuthenticationConverter authenticationConverter = this::createAuthentication;
|
||||
private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendUserInfoResponse;
|
||||
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OidcUserInfoEndpointFilter} using the provided parameters.
|
||||
@@ -100,34 +109,77 @@ public final class OidcUserInfoEndpointFilter extends OncePerRequestFilter {
|
||||
}
|
||||
|
||||
try {
|
||||
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
|
||||
Authentication userInfoAuthentication = this.authenticationConverter.convert(request);
|
||||
|
||||
OidcUserInfoAuthenticationToken userInfoAuthentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
|
||||
OidcUserInfoAuthenticationToken userInfoAuthenticationResult =
|
||||
(OidcUserInfoAuthenticationToken) this.authenticationManager.authenticate(userInfoAuthentication);
|
||||
|
||||
sendUserInfoResponse(response, userInfoAuthenticationResult.getUserInfo());
|
||||
Authentication userInfoAuthenticationResult =
|
||||
this.authenticationManager.authenticate(userInfoAuthentication);
|
||||
|
||||
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, userInfoAuthenticationResult);
|
||||
} catch (OAuth2AuthenticationException ex) {
|
||||
sendErrorResponse(response, ex.getError());
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
|
||||
} catch (Exception ex) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
"OpenID Connect 1.0 UserInfo Error: " + ex.getMessage(),
|
||||
"https://openid.net/specs/openid-connect-core-1_0.html#UserInfoError");
|
||||
sendErrorResponse(response, error);
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
|
||||
new OAuth2AuthenticationException(error));
|
||||
} finally {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendUserInfoResponse(HttpServletResponse response, OidcUserInfo userInfo) throws IOException {
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
this.userInfoHttpMessageConverter.write(userInfo, null, httpResponse);
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract an UserInfo Request from {@link HttpServletRequest}
|
||||
* to an instance of {@link OidcUserInfoAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an UserInfo Request from {@link HttpServletRequest}
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
}
|
||||
|
||||
private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
|
||||
/**
|
||||
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OidcUserInfoAuthenticationToken}
|
||||
* and returning the {@link OidcUserInfo UserInfo Response}.
|
||||
*
|
||||
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OidcUserInfoAuthenticationToken}
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
|
||||
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
|
||||
this.authenticationSuccessHandler = authenticationSuccessHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* and returning the {@link OAuth2Error Error Response}.
|
||||
*
|
||||
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
|
||||
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
|
||||
this.authenticationFailureHandler = authenticationFailureHandler;
|
||||
}
|
||||
|
||||
private Authentication createAuthentication(HttpServletRequest request) {
|
||||
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
|
||||
return new OidcUserInfoAuthenticationToken(principal);
|
||||
}
|
||||
|
||||
private void sendUserInfoResponse(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) throws IOException {
|
||||
OidcUserInfoAuthenticationToken userInfoAuthenticationToken = (OidcUserInfoAuthenticationToken) authentication;
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
this.userInfoHttpMessageConverter.write(userInfoAuthenticationToken.getUserInfo(), null, httpResponse);
|
||||
}
|
||||
|
||||
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authenticationException) throws IOException {
|
||||
OAuth2Error error = ((OAuth2AuthenticationException) authenticationException).getError();
|
||||
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
|
||||
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_TOKEN)) {
|
||||
httpStatus = HttpStatus.UNAUTHORIZED;
|
||||
@@ -138,4 +190,5 @@ public final class OidcUserInfoEndpointFilter extends OncePerRequestFilter {
|
||||
httpResponse.setStatusCode(httpStatus);
|
||||
this.errorHttpResponseConverter.write(error, null, httpResponse);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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.oidc.web.authentication;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
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.server.authorization.oidc.OidcClientRegistration;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Attempts to extract an OpenID Connect 1.0 Dynamic Client Registration (or Client Read) Request from {@link HttpServletRequest}
|
||||
* and then converts to an {@link OidcClientRegistrationAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see AuthenticationConverter
|
||||
* @see OidcClientRegistrationAuthenticationToken
|
||||
* @see OidcClientRegistrationEndpointFilter
|
||||
*/
|
||||
public final class OidcClientRegistrationAuthenticationConverter implements AuthenticationConverter {
|
||||
private final HttpMessageConverter<OidcClientRegistration> clientRegistrationHttpMessageConverter =
|
||||
new OidcClientRegistrationHttpMessageConverter();
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if ("POST".equals(request.getMethod())) {
|
||||
OidcClientRegistration clientRegistration;
|
||||
try {
|
||||
clientRegistration = this.clientRegistrationHttpMessageConverter.read(
|
||||
OidcClientRegistration.class, new ServletServerHttpRequest(request));
|
||||
} catch (Exception ex) {
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
"OpenID Client Registration Error: " + ex.getMessage(),
|
||||
"https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError");
|
||||
throw new OAuth2AuthenticationException(error, ex);
|
||||
}
|
||||
return new OidcClientRegistrationAuthenticationToken(principal, clientRegistration);
|
||||
}
|
||||
|
||||
// client_id (REQUIRED)
|
||||
String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID);
|
||||
if (!StringUtils.hasText(clientId) ||
|
||||
request.getParameterValues(OAuth2ParameterNames.CLIENT_ID).length != 1) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
return new OidcClientRegistrationAuthenticationToken(principal, clientId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,7 +18,9 @@ package org.springframework.security.oauth2.server.authorization.web;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -287,10 +289,16 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
UriComponentsBuilder uriBuilder = UriComponentsBuilder
|
||||
.fromUriString(authorizationCodeRequestAuthentication.getRedirectUri())
|
||||
.queryParam(OAuth2ParameterNames.CODE, authorizationCodeRequestAuthentication.getAuthorizationCode().getTokenValue());
|
||||
String redirectUri;
|
||||
if (StringUtils.hasText(authorizationCodeRequestAuthentication.getState())) {
|
||||
uriBuilder.queryParam(OAuth2ParameterNames.STATE, authorizationCodeRequestAuthentication.getState());
|
||||
uriBuilder.queryParam(OAuth2ParameterNames.STATE, "{state}");
|
||||
Map<String, String> queryParams = new HashMap<>();
|
||||
queryParams.put(OAuth2ParameterNames.STATE, authorizationCodeRequestAuthentication.getState());
|
||||
redirectUri = uriBuilder.build(queryParams).toString();
|
||||
} else {
|
||||
redirectUri = uriBuilder.toUriString();
|
||||
}
|
||||
this.redirectStrategy.sendRedirect(request, response, uriBuilder.toUriString());
|
||||
this.redirectStrategy.sendRedirect(request, response, redirectUri);
|
||||
}
|
||||
|
||||
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
|
||||
@@ -317,10 +325,16 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
if (StringUtils.hasText(error.getUri())) {
|
||||
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_URI, error.getUri());
|
||||
}
|
||||
String redirectUri;
|
||||
if (StringUtils.hasText(authorizationCodeRequestAuthentication.getState())) {
|
||||
uriBuilder.queryParam(OAuth2ParameterNames.STATE, authorizationCodeRequestAuthentication.getState());
|
||||
uriBuilder.queryParam(OAuth2ParameterNames.STATE, "{state}");
|
||||
Map<String, String> queryParams = new HashMap<>();
|
||||
queryParams.put(OAuth2ParameterNames.STATE, authorizationCodeRequestAuthentication.getState());
|
||||
redirectUri = uriBuilder.build(queryParams).toString();
|
||||
} else {
|
||||
redirectUri = uriBuilder.toUriString();
|
||||
}
|
||||
this.redirectStrategy.sendRedirect(request, response, uriBuilder.toUriString());
|
||||
this.redirectStrategy.sendRedirect(request, response, redirectUri);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -84,7 +84,7 @@ public class TestOAuth2Authorizations {
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizedScopes(authorizationRequest.getScopes())
|
||||
.token(authorizationCode)
|
||||
.attribute(OAuth2ParameterNames.STATE, "state")
|
||||
.attribute(OAuth2ParameterNames.STATE, "consent-state")
|
||||
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest)
|
||||
.attribute(Principal.class.getName(),
|
||||
new TestingAuthenticationToken("principal", null, "ROLE_A", "ROLE_B"));
|
||||
|
||||
@@ -41,7 +41,6 @@ import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.oauth2.jose.TestJwks;
|
||||
import org.springframework.security.oauth2.jose.TestKeys;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.BadJwtException;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
@@ -117,6 +116,13 @@ public class JwtClientAssertionAuthenticationProviderTests {
|
||||
.hasMessage("authorizationService cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setJwtDecoderFactoryWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.authenticationProvider.setJwtDecoderFactory(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("jwtDecoderFactory cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsWhenTypeOAuth2ClientAuthenticationTokenThenReturnTrue() {
|
||||
assertThat(this.authenticationProvider.supports(OAuth2ClientAuthenticationToken.class)).isTrue();
|
||||
@@ -181,84 +187,6 @@ public class JwtClientAssertionAuthenticationProviderTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenMissingJwkSetUrlThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null);
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
assertThat(error.getDescription()).isEqualTo("Failed to find a Signature Verifier for Client: '" +
|
||||
registeredClient.getId() + "'. Check to ensure you have configured the JWK Set URL.");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenMissingClientSecretThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientSecret(null)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null);
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
assertThat(error.getDescription()).isEqualTo("Failed to find a Signature Verifier for Client: '" +
|
||||
registeredClient.getId() + "'. Check to ensure you have configured the client secret.");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenMissingSigningAlgorithmThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientSecret(TestKeys.DEFAULT_ENCODED_SECRET_KEY)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
|
||||
.build();
|
||||
// @formatter:on
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null);
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
assertThat(error.getDescription()).isEqualTo("Failed to find a Signature Verifier for Client: '" +
|
||||
registeredClient.getId() + "'. Check to ensure you have configured a valid JWS Algorithm: 'null'.");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenInvalidCredentialsThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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 org.junit.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link JwtClientAssertionDecoderFactory}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class JwtClientAssertionDecoderFactoryTests {
|
||||
private JwtClientAssertionDecoderFactory jwtDecoderFactory = new JwtClientAssertionDecoderFactory();
|
||||
|
||||
@Test
|
||||
public void setJwtValidatorFactoryWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.jwtDecoderFactory.setJwtValidatorFactory(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("jwtValidatorFactory cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDecoderWhenMissingJwkSetUrlThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
assertThatThrownBy(() -> this.jwtDecoderFactory.createDecoder(registeredClient))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
assertThat(error.getDescription()).isEqualTo("Failed to find a Signature Verifier for Client: '" +
|
||||
registeredClient.getId() + "'. Check to ensure you have configured the JWK Set URL.");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDecoderWhenMissingClientSecretThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientSecret(null)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
assertThatThrownBy(() -> this.jwtDecoderFactory.createDecoder(registeredClient))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
assertThat(error.getDescription()).isEqualTo("Failed to find a Signature Verifier for Client: '" +
|
||||
registeredClient.getId() + "'. Check to ensure you have configured the client secret.");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDecoderWhenMissingSigningAlgorithmThenThrowOAuth2AuthenticationException() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
assertThatThrownBy(() -> this.jwtDecoderFactory.createDecoder(registeredClient))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
|
||||
.satisfies(error -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
assertThat(error.getDescription()).isEqualTo("Failed to find a Signature Verifier for Client: '" +
|
||||
registeredClient.getId() + "'. Check to ensure you have configured a valid JWS Algorithm: 'null'.");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -69,6 +69,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
@@ -159,6 +160,9 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
private static final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
|
||||
private static final String S256_CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
|
||||
private static final String AUTHORITIES_CLAIM = "authorities";
|
||||
private static final String STATE_URL_UNENCODED = "awrD0fCnEcTUPFgmyy2SU89HZNcnAJ60ZW6l39YI0KyVjmIZ+004pwm9j55li7BoydXYysH4enZMF21Q";
|
||||
private static final String STATE_URL_ENCODED = "awrD0fCnEcTUPFgmyy2SU89HZNcnAJ60ZW6l39YI0KyVjmIZ%2B004pwm9j55li7BoydXYysH4enZMF21Q";
|
||||
|
||||
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
|
||||
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
|
||||
|
||||
@@ -290,7 +294,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=" + STATE_URL_ENCODED);
|
||||
|
||||
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
@@ -382,7 +386,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=" + STATE_URL_ENCODED);
|
||||
|
||||
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
|
||||
OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
@@ -426,7 +430,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=" + STATE_URL_ENCODED);
|
||||
|
||||
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
|
||||
OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
@@ -502,19 +506,27 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
|
||||
.principalName("user")
|
||||
.build();
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
OAuth2AuthorizationRequest updatedAuthorizationRequest =
|
||||
OAuth2AuthorizationRequest.from(authorizationRequest)
|
||||
.state(STATE_URL_UNENCODED)
|
||||
.build();
|
||||
authorization = OAuth2Authorization.from(authorization)
|
||||
.attribute(OAuth2AuthorizationRequest.class.getName(), updatedAuthorizationRequest)
|
||||
.build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(post(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
||||
.param(OAuth2ParameterNames.SCOPE, "message.read")
|
||||
.param(OAuth2ParameterNames.SCOPE, "message.write")
|
||||
.param(OAuth2ParameterNames.STATE, "state")
|
||||
.param(OAuth2ParameterNames.STATE, authorization.<String>getAttribute(OAuth2ParameterNames.STATE))
|
||||
.with(user("user")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=" + STATE_URL_ENCODED);
|
||||
|
||||
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
|
||||
OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
@@ -582,18 +594,26 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
|
||||
.build();
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
OAuth2AuthorizationRequest updatedAuthorizationRequest =
|
||||
OAuth2AuthorizationRequest.from(authorizationRequest)
|
||||
.state(STATE_URL_UNENCODED)
|
||||
.build();
|
||||
authorization = OAuth2Authorization.from(authorization)
|
||||
.attribute(OAuth2AuthorizationRequest.class.getName(), updatedAuthorizationRequest)
|
||||
.build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(post(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
||||
.param("authority", "authority-1 authority-2")
|
||||
.param(OAuth2ParameterNames.STATE, "state")
|
||||
.param(OAuth2ParameterNames.STATE, authorization.<String>getAttribute(OAuth2ParameterNames.STATE))
|
||||
.with(user("principal")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=" + STATE_URL_ENCODED);
|
||||
|
||||
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
|
||||
OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
@@ -631,7 +651,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
"https://provider.com/oauth2/authorize", registeredClient.getClientId(), principal, authorizationCode,
|
||||
registeredClient.getRedirectUris().iterator().next(), "state", registeredClient.getScopes());
|
||||
registeredClient.getRedirectUris().iterator().next(), STATE_URL_UNENCODED, registeredClient.getScopes());
|
||||
when(authorizationRequestConverter.convert(any())).thenReturn(authorizationCodeRequestAuthenticationResult);
|
||||
when(authorizationRequestAuthenticationProvider.supports(eq(OAuth2AuthorizationCodeRequestAuthenticationToken.class))).thenReturn(true);
|
||||
when(authorizationRequestAuthenticationProvider.authenticate(any())).thenReturn(authorizationCodeRequestAuthenticationResult);
|
||||
@@ -718,7 +738,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
|
||||
parameters.set(OAuth2ParameterNames.SCOPE,
|
||||
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
|
||||
parameters.set(OAuth2ParameterNames.STATE, "state");
|
||||
parameters.set(OAuth2ParameterNames.STATE, STATE_URL_UNENCODED);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ package org.springframework.security.oauth2.server.authorization.config.annotati
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
@@ -30,6 +34,7 @@ import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -37,6 +42,7 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.jdbc.core.JdbcOperations;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
||||
@@ -45,6 +51,7 @@ import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
import org.springframework.mock.http.MockHttpOutputMessage;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -54,6 +61,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
@@ -76,11 +84,18 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcClientRegistrationAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
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;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
@@ -88,6 +103,14 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
@@ -127,6 +150,18 @@ public class OidcClientRegistrationTests {
|
||||
@Autowired
|
||||
private AuthorizationServerSettings authorizationServerSettings;
|
||||
|
||||
private static AuthenticationConverter authenticationConverter;
|
||||
|
||||
private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;
|
||||
|
||||
private static AuthenticationProvider authenticationProvider;
|
||||
|
||||
private static Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer;
|
||||
|
||||
private static AuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
|
||||
private static AuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
private MockWebServer server;
|
||||
private String clientJwkSetUrl;
|
||||
|
||||
@@ -144,6 +179,12 @@ public class OidcClientRegistrationTests {
|
||||
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
|
||||
.addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
|
||||
.build();
|
||||
authenticationConverter = mock(AuthenticationConverter.class);
|
||||
authenticationConvertersConsumer = mock(Consumer.class);
|
||||
authenticationProvider = mock(AuthenticationProvider.class);
|
||||
authenticationProvidersConsumer = mock(Consumer.class);
|
||||
authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
|
||||
authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
|
||||
}
|
||||
|
||||
@Before
|
||||
@@ -157,6 +198,7 @@ public class OidcClientRegistrationTests {
|
||||
.setBody(clientJwkSet.toString());
|
||||
// @formatter:on
|
||||
this.server.enqueue(response);
|
||||
when(authenticationProvider.supports(OidcClientRegistrationAuthenticationToken.class)).thenReturn(true);
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -164,6 +206,12 @@ public class OidcClientRegistrationTests {
|
||||
this.server.shutdown();
|
||||
jdbcOperations.update("truncate table oauth2_authorization");
|
||||
jdbcOperations.update("truncate table oauth2_registered_client");
|
||||
reset(authenticationConverter);
|
||||
reset(authenticationConvertersConsumer);
|
||||
reset(authenticationProvider);
|
||||
reset(authenticationProvidersConsumer);
|
||||
reset(authenticationSuccessHandler);
|
||||
reset(authenticationFailureHandler);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@@ -260,6 +308,67 @@ public class OidcClientRegistrationTests {
|
||||
assertThat(clientConfigurationResponse.getRegistrationAccessToken()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenClientRegistrationEndpointCustomizedThenUsed() throws Exception {
|
||||
this.spring.register(CustomClientRegistrationConfiguration.class).autowire();
|
||||
|
||||
// @formatter:off
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
|
||||
.clientName("client-name")
|
||||
.redirectUri("https://client.example.com")
|
||||
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.scope("scope1")
|
||||
.scope("scope2")
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
doAnswer(invocation -> {
|
||||
HttpServletResponse response = invocation.getArgument(1, HttpServletResponse.class);
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
httpResponse.setStatusCode(HttpStatus.CREATED);
|
||||
new OidcClientRegistrationHttpMessageConverter().write(clientRegistration, null, httpResponse);
|
||||
return null;
|
||||
}).when(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
|
||||
|
||||
registerClient(clientRegistration);
|
||||
|
||||
verify(authenticationConverter).convert(any());
|
||||
ArgumentCaptor<List<AuthenticationConverter>> authenticationConvertersCaptor =
|
||||
ArgumentCaptor.forClass(List.class);
|
||||
verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
|
||||
List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
|
||||
assertThat(authenticationConverters).hasSize(2)
|
||||
.allMatch(converter -> converter == authenticationConverter
|
||||
|| converter instanceof OidcClientRegistrationAuthenticationConverter);
|
||||
|
||||
verify(authenticationProvider).authenticate(any());
|
||||
ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor =
|
||||
ArgumentCaptor.forClass(List.class);
|
||||
verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
|
||||
List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
|
||||
assertThat(authenticationProviders).hasSize(3)
|
||||
.allMatch(provider -> provider == authenticationProvider
|
||||
|| provider instanceof OidcClientRegistrationAuthenticationProvider
|
||||
|| provider instanceof OidcClientConfigurationAuthenticationProvider);
|
||||
|
||||
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
|
||||
verifyNoInteractions(authenticationFailureHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenClientRegistrationEndpointCustomizedWithAuthenticationFailureHandlerThenUsed() throws Exception {
|
||||
this.spring.register(CustomClientRegistrationConfiguration.class).autowire();
|
||||
|
||||
when(authenticationProvider.authenticate(any())).thenThrow(new OAuth2AuthenticationException("error"));
|
||||
|
||||
this.mvc.perform(get(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI)
|
||||
.param(OAuth2ParameterNames.CLIENT_ID, "invalid").with(jwt()));
|
||||
|
||||
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
|
||||
verifyNoInteractions(authenticationSuccessHandler);
|
||||
}
|
||||
|
||||
private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
|
||||
// ***** (1) Obtain the "initial" access token used for registering the client
|
||||
|
||||
@@ -352,6 +461,43 @@ public class OidcClientRegistrationTests {
|
||||
return clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class CustomClientRegistrationConfiguration extends AuthorizationServerConfiguration {
|
||||
|
||||
// @formatter:off
|
||||
@Bean
|
||||
@Override
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer();
|
||||
authorizationServerConfigurer
|
||||
.oidc(oidc ->
|
||||
oidc
|
||||
.clientRegistrationEndpoint(clientRegistration ->
|
||||
clientRegistration
|
||||
.clientRegistrationRequestConverter(authenticationConverter)
|
||||
.clientRegistrationRequestConverters(authenticationConvertersConsumer)
|
||||
.authenticationProvider(authenticationProvider)
|
||||
.authenticationProviders(authenticationProvidersConsumer)
|
||||
.clientRegistrationResponseHandler(authenticationSuccessHandler)
|
||||
.errorResponseHandler(authenticationFailureHandler)
|
||||
)
|
||||
);
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
|
||||
|
||||
http
|
||||
.requestMatcher(endpointsMatcher)
|
||||
.authorizeRequests(authorizeRequests ->
|
||||
authorizeRequests.anyRequest().authenticated()
|
||||
)
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
|
||||
.apply(authorizationServerConfigurer);
|
||||
return http.build();
|
||||
}
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class AuthorizationServerConfiguration {
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -197,9 +196,16 @@ public class OidcProviderConfigurationTests {
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RegisteredClientRepository registeredClientRepository() {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
@@ -275,7 +281,6 @@ public class OidcProviderConfigurationTests {
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfigurationWithInvalidIssuerUrl extends AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -285,7 +290,6 @@ public class OidcProviderConfigurationTests {
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfigurationWithInvalidIssuerUri extends AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -295,7 +299,6 @@ public class OidcProviderConfigurationTests {
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfigurationWithIssuerQuery extends AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -305,7 +308,6 @@ public class OidcProviderConfigurationTests {
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfigurationWithIssuerFragment extends AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -315,7 +317,6 @@ public class OidcProviderConfigurationTests {
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfigurationWithIssuerQueryAndFragment extends AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -325,7 +326,6 @@ public class OidcProviderConfigurationTests {
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfigurationWithIssuerEmptyQuery extends AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -335,7 +335,6 @@ public class OidcProviderConfigurationTests {
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfigurationWithIssuerEmptyFragment extends AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -36,7 +36,6 @@ import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
@@ -48,6 +47,7 @@ import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -79,6 +79,7 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
||||
@@ -276,9 +277,16 @@ public class OidcTests {
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfiguration {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations, RegisteredClientRepository registeredClientRepository) {
|
||||
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository);
|
||||
@@ -324,6 +332,11 @@ public class OidcTests {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthorizationServerSettings authorizationServerSettings() {
|
||||
return AuthorizationServerSettings.builder().build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
PasswordEncoder passwordEncoder() {
|
||||
return NoOpPasswordEncoder.getInstance();
|
||||
@@ -360,7 +373,8 @@ public class OidcTests {
|
||||
http.apply(authorizationServerConfigurer);
|
||||
|
||||
authorizationServerConfigurer
|
||||
.tokenGenerator(tokenGenerator());
|
||||
.tokenGenerator(tokenGenerator())
|
||||
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
||||
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
|
||||
|
||||
|
||||
@@ -19,9 +19,13 @@ import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
@@ -30,10 +34,14 @@ import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.Customizer;
|
||||
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.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
@@ -60,11 +68,15 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
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.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
@@ -73,8 +85,15 @@ import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
@@ -98,17 +117,48 @@ public class OidcUserInfoTests {
|
||||
@Autowired
|
||||
private JwtEncoder jwtEncoder;
|
||||
|
||||
@Autowired
|
||||
private JwtDecoder jwtDecoder;
|
||||
|
||||
@Autowired
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
|
||||
private static AuthenticationConverter authenticationConverter;
|
||||
|
||||
private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;
|
||||
|
||||
private static AuthenticationProvider authenticationProvider;
|
||||
|
||||
private static Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer;
|
||||
|
||||
private static AuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
|
||||
private static AuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
private static Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() {
|
||||
securityContextRepository = spy(new HttpSessionSecurityContextRepository());
|
||||
authenticationConverter = mock(AuthenticationConverter.class);
|
||||
authenticationConvertersConsumer = mock(Consumer.class);
|
||||
authenticationProvider = mock(AuthenticationProvider.class);
|
||||
authenticationProvidersConsumer = mock(Consumer.class);
|
||||
authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
|
||||
authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
|
||||
userInfoMapper = mock(Function.class);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
reset(securityContextRepository);
|
||||
reset(authenticationConverter);
|
||||
reset(authenticationConvertersConsumer);
|
||||
reset(authenticationProvider);
|
||||
reset(authenticationProvidersConsumer);
|
||||
reset(authenticationSuccessHandler);
|
||||
reset(authenticationFailureHandler);
|
||||
reset(userInfoMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -144,19 +194,91 @@ public class OidcUserInfoTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenSignedJwtAndCustomUserInfoMapperThenMapJwtClaimsToUserInfoResponse() throws Exception {
|
||||
public void requestWhenUserInfoEndpointCustomizedThenUsed() throws Exception {
|
||||
this.spring.register(CustomUserInfoConfiguration.class).autowire();
|
||||
|
||||
OAuth2Authorization authorization = createAuthorization();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
when(userInfoMapper.apply(any())).thenReturn(createUserInfo());
|
||||
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpectAll(userInfoResponse());
|
||||
.andExpect(status().is2xxSuccessful());
|
||||
// @formatter:on
|
||||
|
||||
verify(userInfoMapper).apply(any());
|
||||
verify(authenticationConverter).convert(any());
|
||||
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
|
||||
verifyNoInteractions(authenticationFailureHandler);
|
||||
|
||||
ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor.forClass(List.class);
|
||||
verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
|
||||
List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
|
||||
assertThat(authenticationProviders).hasSize(2).allMatch(provider ->
|
||||
provider == authenticationProvider ||
|
||||
provider instanceof OidcUserInfoAuthenticationProvider
|
||||
);
|
||||
|
||||
ArgumentCaptor<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class);
|
||||
verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
|
||||
List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
|
||||
assertThat(authenticationConverters).hasSize(2).allMatch(AuthenticationConverter.class::isInstance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUserInfoEndpointCustomizedWithAuthenticationProviderThenUsed() throws Exception {
|
||||
this.spring.register(CustomUserInfoConfiguration.class).autowire();
|
||||
|
||||
OAuth2Authorization authorization = createAuthorization();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
when(authenticationProvider.supports(eq(OidcUserInfoAuthenticationToken.class))).thenReturn(true);
|
||||
String tokenValue = authorization.getAccessToken().getToken().getTokenValue();
|
||||
Jwt jwt = this.jwtDecoder.decode(tokenValue);
|
||||
OidcUserInfoAuthenticationToken oidcUserInfoAuthentication = new OidcUserInfoAuthenticationToken(
|
||||
new JwtAuthenticationToken(jwt), createUserInfo());
|
||||
when(authenticationProvider.authenticate(any())).thenReturn(oidcUserInfoAuthentication);
|
||||
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
|
||||
.andExpect(status().is2xxSuccessful());
|
||||
// @formatter:on
|
||||
|
||||
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
|
||||
verify(authenticationProvider).authenticate(any());
|
||||
verifyNoInteractions(authenticationFailureHandler);
|
||||
verifyNoInteractions(userInfoMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUserInfoEndpointCustomizedWithAuthenticationFailureHandlerThenUsed() throws Exception {
|
||||
this.spring.register(CustomUserInfoConfiguration.class).autowire();
|
||||
|
||||
when(userInfoMapper.apply(any())).thenReturn(createUserInfo());
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
HttpServletResponse response = invocation.getArgument(1);
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.getWriter().write("unauthorized");
|
||||
return null;
|
||||
}
|
||||
).when(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
|
||||
|
||||
OAuth2AccessToken accessToken = createAuthorization().getAccessToken().getToken();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
|
||||
.andExpect(status().is4xxClientError());
|
||||
// @formatter:on
|
||||
|
||||
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
|
||||
verifyNoInteractions(authenticationSuccessHandler);
|
||||
verifyNoInteractions(userInfoMapper);
|
||||
}
|
||||
|
||||
// gh-482
|
||||
@@ -252,7 +374,7 @@ public class OidcUserInfoTests {
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.phoneNumberVerified("false")
|
||||
.phoneNumberVerified(false)
|
||||
.claim("address", Collections.singletonMap("formatted", "Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"))
|
||||
.updatedAt("1970-01-01T00:00:00Z")
|
||||
.build();
|
||||
@@ -270,14 +392,6 @@ public class OidcUserInfoTests {
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer
|
||||
.getEndpointsMatcher();
|
||||
|
||||
// Custom User Info Mapper that retrieves claims from a signed JWT
|
||||
Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper = context -> {
|
||||
OidcUserInfoAuthenticationToken authentication = context.getAuthentication();
|
||||
JwtAuthenticationToken principal = (JwtAuthenticationToken) authentication.getPrincipal();
|
||||
|
||||
return new OidcUserInfo(principal.getToken().getClaims());
|
||||
};
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.requestMatcher(endpointsMatcher)
|
||||
@@ -289,6 +403,12 @@ public class OidcUserInfoTests {
|
||||
.apply(authorizationServerConfigurer)
|
||||
.oidc(oidc -> oidc
|
||||
.userInfoEndpoint(userInfo -> userInfo
|
||||
.userInfoRequestConverter(authenticationConverter)
|
||||
.userInfoRequestConverters(authenticationConvertersConsumer)
|
||||
.authenticationProvider(authenticationProvider)
|
||||
.authenticationProviders(authenticationProvidersConsumer)
|
||||
.userInfoResponseHandler(authenticationSuccessHandler)
|
||||
.errorResponseHandler(authenticationFailureHandler)
|
||||
.userInfoMapper(userInfoMapper)
|
||||
)
|
||||
);
|
||||
@@ -306,6 +426,8 @@ public class OidcUserInfoTests {
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer();
|
||||
authorizationServerConfigurer
|
||||
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer
|
||||
.getEndpointsMatcher();
|
||||
|
||||
@@ -333,6 +455,8 @@ public class OidcUserInfoTests {
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer();
|
||||
authorizationServerConfigurer
|
||||
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer
|
||||
.getEndpointsMatcher();
|
||||
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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.oidc.authentication;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.TestJwsHeaders;
|
||||
import org.springframework.security.oauth2.jwt.TestJwtClaimsSets;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcClientConfigurationAuthenticationProvider}.
|
||||
*
|
||||
* @author Ovidiu Popa
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class OidcClientConfigurationAuthenticationProviderTests {
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
private AuthorizationServerSettings authorizationServerSettings;
|
||||
private OidcClientConfigurationAuthenticationProvider authenticationProvider;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
this.authorizationServerSettings = AuthorizationServerSettings.builder().issuer("https://provider.com").build();
|
||||
AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(this.authorizationServerSettings, null));
|
||||
this.authenticationProvider = new OidcClientConfigurationAuthenticationProvider(
|
||||
this.registeredClientRepository, this.authorizationService);
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
AuthorizationServerContextHolder.resetContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new OidcClientConfigurationAuthenticationProvider(null, this.authorizationService))
|
||||
.withMessage("registeredClientRepository cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new OidcClientConfigurationAuthenticationProvider(this.registeredClientRepository, null))
|
||||
.withMessage("authorizationService cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsWhenTypeOidcClientRegistrationAuthenticationTokenThenReturnTrue() {
|
||||
assertThat(this.authenticationProvider.supports(OidcClientRegistrationAuthenticationToken.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenPrincipalNotOAuth2TokenAuthenticationTokenThenThrowOAuth2AuthenticationException() {
|
||||
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, "client-id");
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(createJwtClientConfiguration());
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, "client-id");
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenNotFoundThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwtClientConfiguration();
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read"));
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, "client-id");
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
verify(this.authorizationService).findByToken(
|
||||
eq(jwt.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenNotActiveThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwtClientConfiguration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
|
||||
registeredClient, jwtAccessToken, jwt.getClaims()).build();
|
||||
authorization = OidcAuthenticationProviderUtils.invalidate(authorization, jwtAccessToken);
|
||||
when(this.authorizationService.findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read"));
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, registeredClient.getClientId());
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
verify(this.authorizationService).findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenNotAuthorizedThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwt(Collections.singleton("unauthorized.scope"));
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
|
||||
registeredClient, jwtAccessToken, jwt.getClaims()).build();
|
||||
when(this.authorizationService.findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
jwt, AuthorityUtils.createAuthorityList("SCOPE_unauthorized.scope"));
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, registeredClient.getClientId());
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
|
||||
verify(this.authorizationService).findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAccessTokenContainsRequiredScopeAndAdditionalScopeThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwt(new HashSet<>(Arrays.asList("client.read", "scope1")));
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
|
||||
registeredClient, jwtAccessToken, jwt.getClaims()).build();
|
||||
when(this.authorizationService.findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read", "SCOPE_scope1"));
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, registeredClient.getClientId());
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
verify(this.authorizationService).findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenRegisteredClientNotFoundThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwtClientConfiguration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
|
||||
registeredClient, jwtAccessToken, jwt.getClaims()).build();
|
||||
when(this.authorizationService.findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read"));
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, registeredClient.getClientId());
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
verify(this.authorizationService).findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
verify(this.registeredClientRepository).findByClientId(
|
||||
eq(registeredClient.getClientId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientIdNotEqualToAuthorizedClientThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwtClientConfiguration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient()
|
||||
.id("registration-2").clientId("client-2").build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
|
||||
authorizedRegisteredClient, jwtAccessToken, jwt.getClaims()).build();
|
||||
when(this.authorizationService.findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read"));
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, registeredClient.getClientId());
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
verify(this.authorizationService).findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
verify(this.registeredClientRepository).findByClientId(
|
||||
eq(registeredClient.getClientId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
|
||||
Jwt jwt = createJwtClientConfiguration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethods((clientAuthenticationMethods) -> {
|
||||
clientAuthenticationMethods.clear();
|
||||
clientAuthenticationMethods.add(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
|
||||
})
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS512)
|
||||
.jwkSetUrl("https://client.example.com/jwks")
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
|
||||
registeredClient, jwtAccessToken, jwt.getClaims()).build();
|
||||
when(this.authorizationService.findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read"));
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, registeredClient.getClientId());
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authenticationResult =
|
||||
(OidcClientRegistrationAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
||||
|
||||
verify(this.authorizationService).findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
verify(this.registeredClientRepository).findByClientId(
|
||||
eq(registeredClient.getClientId()));
|
||||
|
||||
// verify that the "registration" access token is not invalidated after it is used
|
||||
verify(this.authorizationService, never()).save(eq(authorization));
|
||||
assertThat(authorization.getAccessToken().isInvalidated()).isFalse();
|
||||
|
||||
OidcClientRegistration clientRegistrationResult = authenticationResult.getClientRegistration();
|
||||
assertThat(clientRegistrationResult.getClientId()).isEqualTo(registeredClient.getClientId());
|
||||
assertThat(clientRegistrationResult.getClientIdIssuedAt()).isEqualTo(registeredClient.getClientIdIssuedAt());
|
||||
assertThat(clientRegistrationResult.getClientSecret()).isEqualTo(registeredClient.getClientSecret());
|
||||
assertThat(clientRegistrationResult.getClientSecretExpiresAt()).isEqualTo(registeredClient.getClientSecretExpiresAt());
|
||||
assertThat(clientRegistrationResult.getClientName()).isEqualTo(registeredClient.getClientName());
|
||||
assertThat(clientRegistrationResult.getRedirectUris())
|
||||
.containsExactlyInAnyOrderElementsOf(registeredClient.getRedirectUris());
|
||||
|
||||
List<String> grantTypes = new ArrayList<>();
|
||||
registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType ->
|
||||
grantTypes.add(authorizationGrantType.getValue()));
|
||||
assertThat(clientRegistrationResult.getGrantTypes()).containsExactlyInAnyOrderElementsOf(grantTypes);
|
||||
|
||||
assertThat(clientRegistrationResult.getResponseTypes())
|
||||
.containsExactly(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
assertThat(clientRegistrationResult.getScopes())
|
||||
.containsExactlyInAnyOrderElementsOf(registeredClient.getScopes());
|
||||
assertThat(clientRegistrationResult.getTokenEndpointAuthenticationMethod())
|
||||
.isEqualTo(registeredClient.getClientAuthenticationMethods().iterator().next().getValue());
|
||||
assertThat(clientRegistrationResult.getTokenEndpointAuthenticationSigningAlgorithm())
|
||||
.isEqualTo(registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm().getName());
|
||||
assertThat(clientRegistrationResult.getJwkSetUrl().toString())
|
||||
.isEqualTo(registeredClient.getClientSettings().getJwkSetUrl());
|
||||
assertThat(clientRegistrationResult.getIdTokenSignedResponseAlgorithm())
|
||||
.isEqualTo(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName());
|
||||
|
||||
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
|
||||
String expectedRegistrationClientUrl = UriComponentsBuilder.fromUriString(authorizationServerContext.getIssuer())
|
||||
.path(authorizationServerContext.getAuthorizationServerSettings().getOidcClientRegistrationEndpoint())
|
||||
.queryParam(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()).toUriString();
|
||||
|
||||
assertThat(clientRegistrationResult.getRegistrationClientUrl().toString()).isEqualTo(expectedRegistrationClientUrl);
|
||||
assertThat(clientRegistrationResult.getRegistrationAccessToken()).isNull();
|
||||
}
|
||||
|
||||
private static Jwt createJwtClientConfiguration() {
|
||||
return createJwt(Collections.singleton("client.read"));
|
||||
}
|
||||
|
||||
private static Jwt createJwt(Set<String> scopes) {
|
||||
// @formatter:off
|
||||
JwsHeader jwsHeader = TestJwsHeaders.jwsHeader()
|
||||
.build();
|
||||
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet()
|
||||
.claim(OAuth2ParameterNames.SCOPE, scopes)
|
||||
.build();
|
||||
Jwt jwt = Jwt.withTokenValue("jwt-access-token")
|
||||
.headers(headers -> headers.putAll(jwsHeader.getHeaders()))
|
||||
.claims(claims -> claims.putAll(jwtClaimsSet.getClaims()))
|
||||
.build();
|
||||
// @formatter:on
|
||||
return jwt;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -58,7 +58,6 @@ import org.springframework.security.oauth2.server.authorization.context.TestAuth
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientMetadataClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
@@ -72,7 +71,6 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -136,6 +134,13 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
.withMessage("tokenGenerator cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setRegisteredClientConverterWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.authenticationProvider.setRegisteredClientConverter(null))
|
||||
.withMessage("registeredClientConverter cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsWhenTypeOidcClientRegistrationAuthenticationTokenThenReturnTrue() {
|
||||
assertThat(this.authenticationProvider.supports(OidcClientRegistrationAuthenticationToken.class)).isTrue();
|
||||
@@ -225,7 +230,7 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientRegistrationRequestAndAccessTokenNotAuthorizedThenThrowOAuth2AuthenticationException() {
|
||||
public void authenticateWhenAccessTokenNotAuthorizedThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwt(Collections.singleton("unauthorized.scope"));
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
@@ -255,7 +260,7 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientRegistrationRequestAndAccessTokenContainsRequiredScopeAndAdditionalScopeThenThrowOAuth2AuthenticationException() {
|
||||
public void authenticateWhenAccessTokenContainsRequiredScopeAndAdditionalScopeThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwt(new HashSet<>(Arrays.asList("client.create", "scope1")));
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
@@ -285,7 +290,7 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientRegistrationRequestAndInvalidRedirectUriThenThrowOAuth2AuthenticationException() {
|
||||
public void authenticateWhenInvalidRedirectUriThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
@@ -320,7 +325,7 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientRegistrationRequestAndRedirectUriContainsFragmentThenThrowOAuth2AuthenticationException() {
|
||||
public void authenticateWhenRedirectUriContainsFragmentThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
@@ -355,7 +360,7 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientRegistrationRequestAndInvalidTokenEndpointAuthenticationMethodThenThrowOAuth2AuthenticationException() {
|
||||
public void authenticateWhenInvalidTokenEndpointAuthenticationMethodThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
@@ -434,7 +439,7 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientRegistrationRequestAndTokenEndpointAuthenticationSigningAlgorithmNotProvidedThenDefaults() {
|
||||
public void authenticateWhenTokenEndpointAuthenticationSigningAlgorithmNotProvidedThenDefaults() {
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
@@ -521,7 +526,7 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientRegistrationRequestAndValidAccessTokenThenReturnClientRegistration() {
|
||||
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
@@ -622,202 +627,6 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
assertThat(clientRegistrationResult.getRegistrationAccessToken()).isEqualTo(jwt.getTokenValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientConfigurationRequestAndAccessTokenNotAuthorizedThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwt(Collections.singleton("unauthorized.scope"));
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
|
||||
registeredClient, jwtAccessToken, jwt.getClaims()).build();
|
||||
when(this.authorizationService.findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
jwt, AuthorityUtils.createAuthorityList("SCOPE_unauthorized.scope"));
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, registeredClient.getClientId());
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
|
||||
verify(this.authorizationService).findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientConfigurationRequestAndAccessTokenContainsRequiredScopeAndAdditionalScopeThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwt(new HashSet<>(Arrays.asList("client.read", "scope1")));
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
|
||||
registeredClient, jwtAccessToken, jwt.getClaims()).build();
|
||||
when(this.authorizationService.findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read", "SCOPE_scope1"));
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, registeredClient.getClientId());
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
verify(this.authorizationService).findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientConfigurationRequestAndRegisteredClientNotFoundThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwtClientConfiguration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
|
||||
registeredClient, jwtAccessToken, jwt.getClaims()).build();
|
||||
when(this.authorizationService.findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read"));
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, registeredClient.getClientId());
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
verify(this.authorizationService).findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
verify(this.registeredClientRepository).findByClientId(
|
||||
eq(registeredClient.getClientId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientConfigurationRequestClientIdNotEqualToAuthorizedClientThenThrowOAuth2AuthenticationException() {
|
||||
Jwt jwt = createJwtClientConfiguration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient()
|
||||
.id("registration-2").clientId("client-2").build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
|
||||
authorizedRegisteredClient, jwtAccessToken, jwt.getClaims()).build();
|
||||
when(this.authorizationService.findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read"));
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, registeredClient.getClientId());
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
|
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
verify(this.authorizationService).findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
verify(this.registeredClientRepository).findByClientId(
|
||||
eq(registeredClient.getClientId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenClientConfigurationRequestAndValidAccessTokenThenReturnClientRegistration() {
|
||||
Jwt jwt = createJwtClientConfiguration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(),
|
||||
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientAuthenticationMethods((clientAuthenticationMethods) -> {
|
||||
clientAuthenticationMethods.clear();
|
||||
clientAuthenticationMethods.add(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
|
||||
})
|
||||
.clientSettings(
|
||||
ClientSettings.builder()
|
||||
.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS512)
|
||||
.jwkSetUrl("https://client.example.com/jwks")
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
|
||||
registeredClient, jwtAccessToken, jwt.getClaims()).build();
|
||||
when(this.authorizationService.findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
|
||||
.thenReturn(authorization);
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.read"));
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
|
||||
principal, registeredClient.getClientId());
|
||||
|
||||
OidcClientRegistrationAuthenticationToken authenticationResult =
|
||||
(OidcClientRegistrationAuthenticationToken) this.authenticationProvider.authenticate(authentication);
|
||||
|
||||
verify(this.authorizationService).findByToken(
|
||||
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
|
||||
verify(this.registeredClientRepository).findByClientId(
|
||||
eq(registeredClient.getClientId()));
|
||||
|
||||
// verify that the "registration" access token is not invalidated after it is used
|
||||
verify(this.authorizationService, never()).save(eq(authorization));
|
||||
assertThat(authorization.getAccessToken().isInvalidated()).isFalse();
|
||||
|
||||
OidcClientRegistration clientRegistrationResult = authenticationResult.getClientRegistration();
|
||||
assertThat(clientRegistrationResult.getClientId()).isEqualTo(registeredClient.getClientId());
|
||||
assertThat(clientRegistrationResult.getClientIdIssuedAt()).isEqualTo(registeredClient.getClientIdIssuedAt());
|
||||
assertThat(clientRegistrationResult.getClientSecret()).isEqualTo(registeredClient.getClientSecret());
|
||||
assertThat(clientRegistrationResult.getClientSecretExpiresAt()).isEqualTo(registeredClient.getClientSecretExpiresAt());
|
||||
assertThat(clientRegistrationResult.getClientName()).isEqualTo(registeredClient.getClientName());
|
||||
assertThat(clientRegistrationResult.getRedirectUris())
|
||||
.containsExactlyInAnyOrderElementsOf(registeredClient.getRedirectUris());
|
||||
|
||||
List<String> grantTypes = new ArrayList<>();
|
||||
registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType ->
|
||||
grantTypes.add(authorizationGrantType.getValue()));
|
||||
assertThat(clientRegistrationResult.getGrantTypes()).containsExactlyInAnyOrderElementsOf(grantTypes);
|
||||
|
||||
assertThat(clientRegistrationResult.getResponseTypes())
|
||||
.containsExactly(OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
assertThat(clientRegistrationResult.getScopes())
|
||||
.containsExactlyInAnyOrderElementsOf(registeredClient.getScopes());
|
||||
assertThat(clientRegistrationResult.getTokenEndpointAuthenticationMethod())
|
||||
.isEqualTo(registeredClient.getClientAuthenticationMethods().iterator().next().getValue());
|
||||
assertThat(clientRegistrationResult.getTokenEndpointAuthenticationSigningAlgorithm())
|
||||
.isEqualTo(registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm().getName());
|
||||
assertThat(clientRegistrationResult.getJwkSetUrl().toString())
|
||||
.isEqualTo(registeredClient.getClientSettings().getJwkSetUrl());
|
||||
assertThat(clientRegistrationResult.getIdTokenSignedResponseAlgorithm())
|
||||
.isEqualTo(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName());
|
||||
|
||||
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
|
||||
String expectedRegistrationClientUrl = UriComponentsBuilder.fromUriString(authorizationServerContext.getIssuer())
|
||||
.path(authorizationServerContext.getAuthorizationServerSettings().getOidcClientRegistrationEndpoint())
|
||||
.queryParam(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()).toUriString();
|
||||
|
||||
assertThat(clientRegistrationResult.getRegistrationClientUrl().toString()).isEqualTo(expectedRegistrationClientUrl);
|
||||
assertThat(clientRegistrationResult.getRegistrationAccessToken()).isNull();
|
||||
}
|
||||
|
||||
private static Jwt createJwtClientRegistration() {
|
||||
return createJwt(Collections.singleton("client.create"));
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ public class OidcUserInfoAuthenticationProviderTests {
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.phoneNumberVerified("false")
|
||||
.phoneNumberVerified(false)
|
||||
.claim("address", Collections.singletonMap("formatted", "Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"))
|
||||
.updatedAt("1970-01-01T00:00:00Z")
|
||||
.build();
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@@ -33,6 +35,8 @@ import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
@@ -54,10 +58,14 @@ import org.springframework.security.oauth2.server.authorization.oidc.OidcClientR
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
@@ -68,6 +76,7 @@ import static org.mockito.Mockito.when;
|
||||
*
|
||||
* @author Ovidiu Popa
|
||||
* @author Joe Grandja
|
||||
* @author Daniel Garnier-Moiroux
|
||||
*/
|
||||
public class OidcClientRegistrationEndpointFilterTests {
|
||||
private static final String DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI = "/connect/register";
|
||||
@@ -103,6 +112,27 @@ public class OidcClientRegistrationEndpointFilterTests {
|
||||
.withMessage("clientRegistrationEndpointUri cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAuthenticationConverterWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.filter.setAuthenticationConverter(null))
|
||||
.withMessage("authenticationConverter cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAuthenticationSuccessHandlerWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.filter.setAuthenticationSuccessHandler(null))
|
||||
.withMessage("authenticationSuccessHandler cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAuthenticationFailureHandlerWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.filter.setAuthenticationFailureHandler(null))
|
||||
.withMessage("authenticationFailureHandler cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenNotClientRegistrationRequestThenNotProcessed() throws Exception {
|
||||
String requestUri = "/path";
|
||||
@@ -203,25 +233,13 @@ public class OidcClientRegistrationEndpointFilterTests {
|
||||
@Test
|
||||
public void doFilterWhenClientRegistrationRequestValidThenSuccessResponse() throws Exception {
|
||||
// @formatter:off
|
||||
OidcClientRegistration.Builder clientRegistrationBuilder = OidcClientRegistration.builder()
|
||||
.clientName("client-name")
|
||||
.redirectUri("https://client.example.com")
|
||||
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.scope("scope1")
|
||||
.scope("scope2");
|
||||
OidcClientRegistration expectedClientRegistrationResponse = createClientRegistration();
|
||||
|
||||
OidcClientRegistration clientRegistrationRequest = clientRegistrationBuilder.build();
|
||||
|
||||
OidcClientRegistration expectedClientRegistrationResponse = clientRegistrationBuilder
|
||||
.clientId("client-id")
|
||||
.clientIdIssuedAt(Instant.now())
|
||||
.clientSecret("client-secret")
|
||||
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
|
||||
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
|
||||
.idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName())
|
||||
.registrationAccessToken("registration-access-token")
|
||||
.registrationClientUrl("https://auth-server:9000/connect/register?client_id=client-id")
|
||||
OidcClientRegistration clientRegistrationRequest = OidcClientRegistration.builder()
|
||||
.clientName(expectedClientRegistrationResponse.getClientName())
|
||||
.redirectUris(redirectUris -> redirectUris.addAll(expectedClientRegistrationResponse.getRedirectUris()))
|
||||
.grantTypes(grantTypes -> grantTypes.addAll(expectedClientRegistrationResponse.getGrantTypes()))
|
||||
.scopes(scopes -> scopes.addAll(expectedClientRegistrationResponse.getScopes()))
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
@@ -384,23 +402,7 @@ public class OidcClientRegistrationEndpointFilterTests {
|
||||
|
||||
@Test
|
||||
public void doFilterWhenClientConfigurationRequestValidThenSuccessResponse() throws Exception {
|
||||
// @formatter:off
|
||||
OidcClientRegistration expectedClientRegistrationResponse = OidcClientRegistration.builder()
|
||||
.clientId("client-id")
|
||||
.clientIdIssuedAt(Instant.now())
|
||||
.clientSecret("client-secret")
|
||||
.clientName("client-name")
|
||||
.redirectUri("https://client.example.com")
|
||||
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
|
||||
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
|
||||
.idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName())
|
||||
.scope("scope1")
|
||||
.scope("scope2")
|
||||
.registrationClientUrl("https://auth-server:9000/connect/register?client_id=client-id")
|
||||
.build();
|
||||
// @formatter:on
|
||||
OidcClientRegistration expectedClientRegistrationResponse = createClientRegistration();
|
||||
|
||||
Jwt jwt = createJwt("client.read");
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
@@ -452,6 +454,74 @@ public class OidcClientRegistrationEndpointFilterTests {
|
||||
.isEqualTo(expectedClientRegistrationResponse.getRegistrationClientUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenCustomAuthenticationConverterThenUsed() throws ServletException, IOException {
|
||||
AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class);
|
||||
this.filter.setAuthenticationConverter(authenticationConverter);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.setParameter(OAuth2ParameterNames.CLIENT_ID, "client-id");
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(authenticationConverter).convert(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception {
|
||||
OidcClientRegistration expectedClientRegistrationResponse = createClientRegistration();
|
||||
Authentication principal = new TestingAuthenticationToken("principal", "Credentials");
|
||||
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthenticationResult =
|
||||
new OidcClientRegistrationAuthenticationToken(principal, expectedClientRegistrationResponse);
|
||||
|
||||
when(this.authenticationManager.authenticate(any())).thenReturn(clientRegistrationAuthenticationResult);
|
||||
AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class);
|
||||
this.filter.setAuthenticationSuccessHandler(successHandler);
|
||||
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
securityContext.setAuthentication(principal);
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.setParameter(OAuth2ParameterNames.CLIENT_ID, expectedClientRegistrationResponse.getClientId());
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(successHandler).onAuthenticationSuccess(request, response, clientRegistrationAuthenticationResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception {
|
||||
AuthenticationFailureHandler authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
|
||||
this.filter.setAuthenticationFailureHandler(authenticationFailureHandler);
|
||||
|
||||
when(this.authenticationManager.authenticate(any()))
|
||||
.thenThrow(new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN));
|
||||
|
||||
String requestUri = DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.setParameter(OAuth2ParameterNames.CLIENT_ID, "client1");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(authenticationFailureHandler).onAuthenticationFailure(eq(request), eq(response),
|
||||
any(OAuth2AuthenticationException.class));
|
||||
}
|
||||
|
||||
private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
|
||||
MockClientHttpResponse httpResponse = new MockClientHttpResponse(
|
||||
response.getContentAsByteArray(), HttpStatus.valueOf(response.getStatus()));
|
||||
@@ -471,6 +541,27 @@ public class OidcClientRegistrationEndpointFilterTests {
|
||||
return this.clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
|
||||
}
|
||||
|
||||
private static OidcClientRegistration createClientRegistration() {
|
||||
// @formatter:off
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
|
||||
.clientId("client-id")
|
||||
.clientIdIssuedAt(Instant.now())
|
||||
.clientSecret("client-secret")
|
||||
.clientName("client-name")
|
||||
.redirectUri("https://client.example.com")
|
||||
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
|
||||
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
|
||||
.idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName())
|
||||
.scope("scope1")
|
||||
.scope("scope2")
|
||||
.registrationClientUrl("https://auth-server:9000/connect/register?client_id=client-id")
|
||||
.build();
|
||||
return clientRegistration;
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static Jwt createJwt(String scope) {
|
||||
// @formatter:off
|
||||
JwsHeader jwsHeader = TestJwsHeaders.jwsHeader()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
* Copyright 2020-2022 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.
|
||||
@@ -44,6 +44,9 @@ import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
@@ -84,6 +87,27 @@ public class OidcUserInfoEndpointFilterTests {
|
||||
.withMessage("userInfoEndpointUri cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAuthenticationConverterWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.filter.setAuthenticationConverter(null))
|
||||
.withMessage("authenticationConverter cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAuthenticationSuccessHandlerWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.filter.setAuthenticationSuccessHandler(null))
|
||||
.withMessage("authenticationSuccessHandler cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAuthenticationFailureHandlerWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.filter.setAuthenticationFailureHandler(null))
|
||||
.withMessage("authenticationFailureHandler cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenNotUserInfoRequestThenNotProcessed() throws Exception {
|
||||
String requestUri = "/path";
|
||||
@@ -145,11 +169,21 @@ public class OidcUserInfoEndpointFilterTests {
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserInfoRequestInvalidTokenThenUnauthorizedError() throws Exception {
|
||||
doFilterWhenAuthenticationExceptionThenError(OAuth2ErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserInfoRequestInsufficientScopeThenForbiddenError() throws Exception {
|
||||
doFilterWhenAuthenticationExceptionThenError(OAuth2ErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
private void doFilterWhenAuthenticationExceptionThenError(String oauth2ErrorCode, HttpStatus httpStatus)
|
||||
throws Exception {
|
||||
Authentication principal = new TestingAuthenticationToken("principal", "credentials");
|
||||
SecurityContextHolder.getContext().setAuthentication(principal);
|
||||
|
||||
when(this.authenticationManager.authenticate(any()))
|
||||
.thenThrow(new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN));
|
||||
.thenThrow(new OAuth2AuthenticationException(oauth2ErrorCode));
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
@@ -161,9 +195,82 @@ public class OidcUserInfoEndpointFilterTests {
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value());
|
||||
assertThat(response.getStatus()).isEqualTo(httpStatus.value());
|
||||
OAuth2Error error = readError(response);
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
assertThat(error.getErrorCode()).isEqualTo(oauth2ErrorCode);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception {
|
||||
Authentication principal = new TestingAuthenticationToken("principal", "credentials");
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class);
|
||||
this.filter.setAuthenticationConverter(authenticationConverter);
|
||||
|
||||
when(authenticationConverter.convert(any())).thenReturn(authentication);
|
||||
when(this.authenticationManager.authenticate(any())).thenReturn(
|
||||
new OidcUserInfoAuthenticationToken(principal, createUserInfo())
|
||||
);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
verify(authenticationConverter).convert(request);
|
||||
verify(this.authenticationManager).authenticate(authentication);
|
||||
assertUserInfoResponse(response.getContentAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception {
|
||||
AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class);
|
||||
this.filter.setAuthenticationSuccessHandler(successHandler);
|
||||
|
||||
Authentication principal = new TestingAuthenticationToken("principal", "credentials");
|
||||
SecurityContextHolder.getContext().setAuthentication(principal);
|
||||
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal, createUserInfo());
|
||||
when(this.authenticationManager.authenticate(any())).thenReturn(authentication);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
verify(successHandler).onAuthenticationSuccess(request, response, authentication);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception {
|
||||
AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
|
||||
this.filter.setAuthenticationFailureHandler(failureHandler);
|
||||
|
||||
Authentication principal = new TestingAuthenticationToken("principal", "credentials");
|
||||
SecurityContextHolder.getContext().setAuthentication(principal);
|
||||
|
||||
OAuth2AuthenticationException authenticationException =
|
||||
new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
when(this.authenticationManager.authenticate(any())).thenThrow(authenticationException);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
verify(failureHandler).onAuthenticationFailure(request, response, authenticationException);
|
||||
}
|
||||
|
||||
private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
|
||||
@@ -204,7 +311,7 @@ public class OidcUserInfoEndpointFilterTests {
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.phoneNumberVerified("false")
|
||||
.phoneNumberVerified(false)
|
||||
.address("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance")
|
||||
.updatedAt("1970-01-01T00:00:00Z")
|
||||
.build();
|
||||
@@ -228,7 +335,7 @@ public class OidcUserInfoEndpointFilterTests {
|
||||
assertThat(userInfoResponse).contains("\"zoneinfo\":\"Europe/Paris\"");
|
||||
assertThat(userInfoResponse).contains("\"locale\":\"en-US\"");
|
||||
assertThat(userInfoResponse).contains("\"phone_number\":\"+1 (604) 555-1234;ext=5678\"");
|
||||
assertThat(userInfoResponse).contains("\"phone_number_verified\":\"false\"");
|
||||
assertThat(userInfoResponse).contains("\"phone_number_verified\":false");
|
||||
assertThat(userInfoResponse).contains("\"address\":\"Champ de Mars\\n5 Av. Anatole France\\n75007 Paris\\nFrance\"");
|
||||
assertThat(userInfoResponse).contains("\"updated_at\":\"1970-01-01T00:00:00Z\"");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id "org.springframework.boot" version "2.7.0"
|
||||
id "org.springframework.boot" version "2.7.5"
|
||||
id "io.spring.dependency-management" version "1.0.11.RELEASE"
|
||||
id "java"
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
@@ -61,7 +62,8 @@ public class AuthorizationServerConfig {
|
||||
new OAuth2AuthorizationServerConfigurer();
|
||||
authorizationServerConfigurer
|
||||
.authorizationEndpoint(authorizationEndpoint ->
|
||||
authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI));
|
||||
authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
|
||||
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
||||
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer
|
||||
.getEndpointsMatcher();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id "org.springframework.boot" version "2.7.0"
|
||||
id "org.springframework.boot" version "2.7.5"
|
||||
id "io.spring.dependency-management" version "1.0.11.RELEASE"
|
||||
id "java"
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
@@ -45,6 +46,7 @@ import org.springframework.security.oauth2.server.authorization.client.JdbcRegis
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
@@ -61,6 +63,9 @@ public class AuthorizationServerConfig {
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.exceptionHandling(exceptions ->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id "org.springframework.boot" version "2.7.0"
|
||||
id "org.springframework.boot" version "2.7.5"
|
||||
id "io.spring.dependency-management" version "1.0.11.RELEASE"
|
||||
id "java"
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
@@ -47,6 +48,7 @@ import org.springframework.security.oauth2.server.authorization.client.JdbcRegis
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
||||
@@ -64,6 +66,8 @@ public class AuthorizationServerConfig {
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
||||
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
|
||||
http.apply(new FederatedIdentityConfigurer());
|
||||
return http.build();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id "org.springframework.boot" version "2.7.0"
|
||||
id "org.springframework.boot" version "2.7.5"
|
||||
id "io.spring.dependency-management" version "1.0.11.RELEASE"
|
||||
id "java"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id "org.springframework.boot" version "2.7.0"
|
||||
id "org.springframework.boot" version "2.7.5"
|
||||
id "io.spring.dependency-management" version "1.0.11.RELEASE"
|
||||
id "java"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user