Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e332a9dac5 | ||
|
|
9e0b52ea91 | ||
|
|
02d0292cb1 | ||
|
|
e9c21235a4 | ||
|
|
1d6ca0dbb1 | ||
|
|
4e63c3b6b6 | ||
|
|
8ed0194744 | ||
|
|
fcbb5c1197 | ||
|
|
a9371e918a | ||
|
|
8c78a5bdee | ||
|
|
74fe63a65b | ||
|
|
2c4bd29f98 | ||
|
|
7e03795c58 | ||
|
|
8b0e7578df | ||
|
|
0e509333bc | ||
|
|
061badf3ae | ||
|
|
70466b74d8 | ||
|
|
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 | ||
|
|
92dbcf29a5 | ||
|
|
26aed3c183 | ||
|
|
cd6f1d7dc3 | ||
|
|
4d94e7095d | ||
|
|
80b01854f2 | ||
|
|
c326b1a2ba | ||
|
|
1db05991af | ||
|
|
2cc603c7e7 | ||
|
|
07d69cbfb4 | ||
|
|
502fa24cfb | ||
|
|
4466cbe69d | ||
|
|
8043b8c949 | ||
|
|
0994a1e1e1 | ||
|
|
70d433a45a | ||
|
|
2dabfa02e0 | ||
|
|
6b66719a83 | ||
|
|
aebc613862 | ||
|
|
f583668a9c | ||
|
|
3efee494ad | ||
|
|
c60ae4532f | ||
|
|
4066c3ec4d |
@@ -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-07#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].
|
||||
|
||||
@@ -19,7 +19,7 @@ The feature list can be viewed in the https://docs.spring.io/spring-authorizatio
|
||||
The Spring Authorization Server project provides software support and is documented in its link:SUPPORT_POLICY.adoc[support policy].
|
||||
|
||||
== Getting Started
|
||||
The first place to start is to read the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01[OAuth 2.1 Authorization Framework] to gain an in-depth understanding on how to build an Authorization Server.
|
||||
The first place to start is to read the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07[OAuth 2.1 Authorization Framework] to gain an in-depth understanding on how to build an Authorization Server.
|
||||
It is a critically important first step as the implementation must conform to the specification defined in the OAuth 2.1 Authorization Framework and the https://github.com/spring-projects/spring-authorization-server/wiki/OAuth-2.0-Specifications[related specifications].
|
||||
|
||||
The second place to start is to become very familiar with the codebase in the following Spring Security modules:
|
||||
@@ -35,9 +35,9 @@ The goal is to leverage all the knowledge learned thus far and apply the same to
|
||||
Submitted work via pull requests should follow the same coding style/conventions and adopt the same or similar design patterns that have been established in Spring Security's OAuth 2.0 support.
|
||||
|
||||
== Documentation
|
||||
Be sure to read the https://docs.spring.io/spring-security/reference[Spring Security Reference], as well as the https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html[OAuth 2.0 Reference], which describes the Client and Resource Server features available.
|
||||
Be sure to read the https://docs.spring.io/spring-authorization-server/docs/current/reference/html/[Spring Authorization Server Reference] and https://docs.spring.io/spring-security/reference[Spring Security Reference], as well as the https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html[OAuth 2.0 Reference], which describes the Client and Resource Server features available.
|
||||
|
||||
Extensive JavaDoc for the Spring Security code is also available in the https://docs.spring.io/spring-security/site/docs/current/api/[Spring Security API Documentation].
|
||||
JavaDoc is also available for the https://docs.spring.io/spring-authorization-server/docs/current/api/[Spring Authorization Server API] and https://docs.spring.io/spring-security/site/docs/current/api/[Spring Security API].
|
||||
|
||||
== Code of Conduct
|
||||
This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct].
|
||||
|
||||
@@ -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.24"
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.plugins.PluginManager;
|
||||
import org.gradle.api.tasks.compile.CompileOptions;
|
||||
import org.gradle.api.tasks.compile.JavaCompile;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
import org.gradle.jvm.tasks.Jar;
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper;
|
||||
|
||||
@@ -92,5 +93,8 @@ public class SpringJavaPlugin implements Plugin<Project> {
|
||||
attributes.put("Automatic-Module-Name", project.getName().replace("-", "."));
|
||||
manifest.attributes(attributes);
|
||||
}));
|
||||
project.getTasks().withType(Test.class, (test) -> {
|
||||
test.useJUnitPlatform();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.14.0")
|
||||
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.junit.jupiter:junit-jupiter:5.9.1"
|
||||
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].
|
||||
|
||||
@@ -91,16 +109,18 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
.registeredClientRepository(registeredClientRepository) <1>
|
||||
.authorizationService(authorizationService) <2>
|
||||
.authorizationConsentService(authorizationConsentService) <3>
|
||||
.providerSettings(providerSettings) <4>
|
||||
.authorizationServerSettings(authorizationServerSettings) <4>
|
||||
.tokenGenerator(tokenGenerator) <5>
|
||||
.clientAuthentication(clientAuthentication -> { }) <6>
|
||||
.authorizationEndpoint(authorizationEndpoint -> { }) <7>
|
||||
.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();
|
||||
@@ -109,26 +129,28 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
<1> `registeredClientRepository()`: The xref:core-model-components.adoc#registered-client-repository[`RegisteredClientRepository`] (*REQUIRED*) for managing new and existing clients.
|
||||
<2> `authorizationService()`: The xref:core-model-components.adoc#oauth2-authorization-service[`OAuth2AuthorizationService`] for managing new and existing authorizations.
|
||||
<3> `authorizationConsentService()`: The xref:core-model-components.adoc#oauth2-authorization-consent-service[`OAuth2AuthorizationConsentService`] for managing new and existing authorization consents.
|
||||
<4> `providerSettings()`: The <<configuring-provider-settings, `ProviderSettings`>> (*REQUIRED*) for customizing configuration settings for the OAuth2 authorization server.
|
||||
<4> `authorizationServerSettings()`: The <<configuring-authorization-server-settings, `AuthorizationServerSettings`>> (*REQUIRED*) for customizing configuration settings for the OAuth2 authorization server.
|
||||
<5> `tokenGenerator()`: The xref:core-model-components.adoc#oauth2-token-generator[`OAuth2TokenGenerator`] for generating tokens supported by the OAuth2 authorization server.
|
||||
<6> `clientAuthentication()`: The configurer for <<configuring-client-authentication, OAuth2 Client Authentication>>.
|
||||
<7> `authorizationEndpoint()`: The configurer for the xref:protocol-endpoints.adoc#oauth2-authorization-endpoint[OAuth2 Authorization endpoint].
|
||||
<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-provider-settings]]
|
||||
== Configuring Provider Settings
|
||||
[[configuring-authorization-server-settings]]
|
||||
== Configuring Authorization Server Settings
|
||||
|
||||
`ProviderSettings` contains the configuration settings for the OAuth2 authorization server (provider).
|
||||
`AuthorizationServerSettings` contains the configuration settings for the OAuth2 authorization server.
|
||||
It specifies the `URI` for the protocol endpoints as well as the https://datatracker.ietf.org/doc/html/rfc8414#section-2[issuer identifier].
|
||||
The default `URI` for the protocol endpoints are as follows:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public final class ProviderSettings extends AbstractSettings {
|
||||
public final class AuthorizationServerSettings extends AbstractSettings {
|
||||
|
||||
...
|
||||
|
||||
@@ -149,18 +171,18 @@ public final class ProviderSettings extends AbstractSettings {
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
`ProviderSettings` is a *REQUIRED* component.
|
||||
`AuthorizationServerSettings` is a *REQUIRED* component.
|
||||
|
||||
[TIP]
|
||||
<<default-configuration, `@Import(OAuth2AuthorizationServerConfiguration.class)`>> automatically registers a `ProviderSettings` `@Bean`, if not already provided.
|
||||
<<default-configuration, `@Import(OAuth2AuthorizationServerConfiguration.class)`>> automatically registers an `AuthorizationServerSettings` `@Bean`, if not already provided.
|
||||
|
||||
The following example shows how to customize the configuration settings and register a `ProviderSettings` `@Bean`:
|
||||
The following example shows how to customize the configuration settings and register an `AuthorizationServerSettings` `@Bean`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public ProviderSettings providerSettings() {
|
||||
return ProviderSettings.builder()
|
||||
public AuthorizationServerSettings authorizationServerSettings() {
|
||||
return AuthorizationServerSettings.builder()
|
||||
.issuer("https://example.com")
|
||||
.authorizationEndpoint("/oauth2/v1/authorize")
|
||||
.tokenEndpoint("/oauth2/v1/token")
|
||||
@@ -173,17 +195,14 @@ public ProviderSettings providerSettings() {
|
||||
}
|
||||
----
|
||||
|
||||
The `ProviderContext` is a context object that holds information about the provider.
|
||||
It provides access to the `ProviderSettings` and the "`current`" issuer identifier.
|
||||
The `AuthorizationServerContext` is a context object that holds information of the Authorization Server runtime environment.
|
||||
It provides access to the `AuthorizationServerSettings` and the "`current`" issuer identifier.
|
||||
|
||||
[NOTE]
|
||||
If the issuer identifier is not configured in `ProviderSettings.builder().issuer(String)`, it is resolved from the current request.
|
||||
If the issuer identifier is not configured in `AuthorizationServerSettings.builder().issuer(String)`, it is resolved from the current request.
|
||||
|
||||
[NOTE]
|
||||
The `ProviderContext` is accessible through the `ProviderContextHolder`, which associates it with the current request thread by using a `ThreadLocal`.
|
||||
|
||||
[NOTE]
|
||||
The `ProviderContextFilter` associates the `ProviderContext` with the `ProviderContextHolder`.
|
||||
The `AuthorizationServerContext` is accessible through the `AuthorizationServerContextHolder`, which associates it with the current request thread by using a `ThreadLocal`.
|
||||
|
||||
[[configuring-client-authentication]]
|
||||
== Configuring Client Authentication
|
||||
@@ -205,18 +224,22 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
.clientAuthentication(clientAuthentication ->
|
||||
clientAuthentication
|
||||
.authenticationConverter(authenticationConverter) <1>
|
||||
.authenticationProvider(authenticationProvider) <2>
|
||||
.authenticationSuccessHandler(authenticationSuccessHandler) <3>
|
||||
.errorResponseHandler(errorResponseHandler) <4>
|
||||
.authenticationConverters(authenticationConvertersConsumer) <2>
|
||||
.authenticationProvider(authenticationProvider) <3>
|
||||
.authenticationProviders(authenticationProvidersConsumer) <4>
|
||||
.authenticationSuccessHandler(authenticationSuccessHandler) <5>
|
||||
.errorResponseHandler(errorResponseHandler) <6>
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
<1> `authenticationConverter()`: The `AuthenticationConverter` (_pre-processor_) used when attempting to extract client credentials from `HttpServletRequest` to an instance of `OAuth2ClientAuthenticationToken`.
|
||||
<2> `authenticationProvider()`: The `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2ClientAuthenticationToken`. (One or more may be added to replace the defaults.)
|
||||
<3> `authenticationSuccessHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling a successful client authentication and associating the `OAuth2ClientAuthenticationToken` to the `SecurityContext`.
|
||||
<4> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling a failed client authentication and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.2[`OAuth2Error` response].
|
||||
<1> `authenticationConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract client credentials from `HttpServletRequest` to an instance of `OAuth2ClientAuthenticationToken`.
|
||||
<2> `authenticationConverters()`: 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 `OAuth2ClientAuthenticationToken`.
|
||||
<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> `authenticationSuccessHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling a successful client authentication and associating the `OAuth2ClientAuthenticationToken` to the `SecurityContext`.
|
||||
<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling a failed client authentication and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.2[`OAuth2Error` response].
|
||||
|
||||
`OAuth2ClientAuthenticationConfigurer` configures the `OAuth2ClientAuthenticationFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OAuth2ClientAuthenticationFilter` is the `Filter` that processes client authentication requests.
|
||||
@@ -230,3 +253,55 @@ The supported client authentication methods are `client_secret_basic`, `client_s
|
||||
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `JwtClientAssertionAuthenticationProvider`, `ClientSecretAuthenticationProvider`, and `PublicClientAuthenticationProvider`.
|
||||
* `*AuthenticationSuccessHandler*` -- An internal implementation that associates the "`authenticated`" `OAuth2ClientAuthenticationToken` (current `Authentication`) to the `SecurityContext`.
|
||||
* `*AuthenticationFailureHandler*` -- An internal implementation that uses the `OAuth2Error` associated with the `OAuth2AuthenticationException` to return the OAuth2 error response.
|
||||
|
||||
[[configuring-client-authentication-customizing-jwt-client-assertion-validation]]
|
||||
=== Customizing Jwt Client Assertion Validation
|
||||
|
||||
`JwtClientAssertionDecoderFactory.DEFAULT_JWT_VALIDATOR_FACTORY` is the default factory that provides an `OAuth2TokenValidator<Jwt>` for the specified `RegisteredClient` and is used for validating the `iss`, `sub`, `aud`, `exp` and `nbf` claims of the `Jwt` client assertion.
|
||||
|
||||
`JwtClientAssertionDecoderFactory` provides the ability to override the default `Jwt` client assertion validation by supplying a custom factory of type `Function<RegisteredClient, OAuth2TokenValidator<Jwt>>` to `setJwtValidatorFactory()`.
|
||||
|
||||
[NOTE]
|
||||
`JwtClientAssertionDecoderFactory` is the default `JwtDecoderFactory` used by `JwtClientAssertionAuthenticationProvider` that provides a `JwtDecoder` for the specified `RegisteredClient` and is used for authenticating a `Jwt` Bearer Token during OAuth2 client authentication.
|
||||
|
||||
A common use case for customizing `JwtClientAssertionDecoderFactory` is to validate additional claims in the `Jwt` client assertion.
|
||||
|
||||
The following example shows how to configure `JwtClientAssertionAuthenticationProvider` with a customized `JwtClientAssertionDecoderFactory` that validates an additional claim in the `Jwt` client assertion:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer();
|
||||
http.apply(authorizationServerConfigurer);
|
||||
|
||||
authorizationServerConfigurer
|
||||
.clientAuthentication(clientAuthentication ->
|
||||
clientAuthentication
|
||||
.authenticationProviders(configureJwtClientAssertionValidator())
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private Consumer<List<AuthenticationProvider>> configureJwtClientAssertionValidator() {
|
||||
return (authenticationProviders) ->
|
||||
authenticationProviders.forEach((authenticationProvider) -> {
|
||||
if (authenticationProvider instanceof JwtClientAssertionAuthenticationProvider) {
|
||||
// Customize JwtClientAssertionDecoderFactory
|
||||
JwtClientAssertionDecoderFactory jwtDecoderFactory = new JwtClientAssertionDecoderFactory();
|
||||
Function<RegisteredClient, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = (registeredClient) ->
|
||||
new DelegatingOAuth2TokenValidator<>(
|
||||
// Use default validators
|
||||
JwtClientAssertionDecoderFactory.DEFAULT_JWT_VALIDATOR_FACTORY.apply(registeredClient),
|
||||
// Add custom validator
|
||||
new JwtClaimValidator<>("claim", "value"::equals));
|
||||
jwtDecoderFactory.setJwtValidatorFactory(jwtValidatorFactory);
|
||||
|
||||
((JwtClientAssertionAuthenticationProvider) authenticationProvider)
|
||||
.setJwtDecoderFactory(jwtDecoderFactory);
|
||||
}
|
||||
});
|
||||
}
|
||||
----
|
||||
|
||||
@@ -163,8 +163,9 @@ public class OAuth2Authorization implements Serializable {
|
||||
private String registeredClientId; <2>
|
||||
private String principalName; <3>
|
||||
private AuthorizationGrantType authorizationGrantType; <4>
|
||||
private Map<Class<? extends OAuth2Token>, Token<?>> tokens; <5>
|
||||
private Map<String, Object> attributes; <6>
|
||||
private Set<String> authorizedScopes; <5>
|
||||
private Map<Class<? extends OAuth2Token>, Token<?>> tokens; <6>
|
||||
private Map<String, Object> attributes; <7>
|
||||
|
||||
...
|
||||
|
||||
@@ -174,8 +175,9 @@ public class OAuth2Authorization implements Serializable {
|
||||
<2> `registeredClientId`: The ID that uniquely identifies the <<registered-client, RegisteredClient>>.
|
||||
<3> `principalName`: The principal name of the resource owner (or client).
|
||||
<4> `authorizationGrantType`: The `AuthorizationGrantType` used.
|
||||
<5> `tokens`: The `OAuth2Token` instances (and associated metadata) specific to the executed authorization grant type.
|
||||
<6> `attributes`: The additional attributes specific to the executed authorization grant type – for example, the authenticated `Principal`, `OAuth2AuthorizationRequest`, authorized scope(s), and others.
|
||||
<5> `authorizedScopes`: The `Set` of scope(s) authorized for the client.
|
||||
<6> `tokens`: The `OAuth2Token` instances (and associated metadata) specific to the executed authorization grant type.
|
||||
<7> `attributes`: The additional attributes specific to the executed authorization grant type – for example, the authenticated `Principal`, `OAuth2AuthorizationRequest`, and others.
|
||||
|
||||
`OAuth2Authorization` and its associated `OAuth2Token` instances have a set lifespan.
|
||||
A newly issued `OAuth2Token` is active and becomes inactive when it either expires or is invalidated (revoked).
|
||||
@@ -316,7 +318,7 @@ public interface OAuth2TokenContext extends Context {
|
||||
|
||||
default <T extends Authentication> T getPrincipal() ... <2>
|
||||
|
||||
default ProviderContext getProviderContext() ... <3>
|
||||
default AuthorizationServerContext getAuthorizationServerContext() ... <3>
|
||||
|
||||
@Nullable
|
||||
default OAuth2Authorization getAuthorization() ... <4>
|
||||
@@ -335,7 +337,7 @@ public interface OAuth2TokenContext extends Context {
|
||||
----
|
||||
<1> `getRegisteredClient()`: The <<registered-client, RegisteredClient>> associated with the authorization grant.
|
||||
<2> `getPrincipal()`: The `Authentication` instance of the resource owner (or client).
|
||||
<3> `getProviderContext()`: The xref:configuration-model.adoc#configuring-provider-settings[`ProviderContext`] object that holds information related to the provider.
|
||||
<3> `getAuthorizationServerContext()`: The xref:configuration-model.adoc#configuring-authorization-server-settings[`AuthorizationServerContext`] object that holds information of the Authorization Server runtime environment.
|
||||
<4> `getAuthorization()`: The <<oauth2-authorization, OAuth2Authorization>> associated with the authorization grant.
|
||||
<5> `getAuthorizedScopes()`: The scope(s) authorized for the client.
|
||||
<6> `getTokenType()`: The `OAuth2TokenType` to generate. The supported values are `code`, `access_token`, `refresh_token`, and `id_token`.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -32,18 +32,21 @@ import org.springframework.context.annotation.Configuration;
|
||||
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.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
|
||||
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.settings.ProviderSettings;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
@@ -56,6 +59,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
|
||||
@@ -63,7 +68,9 @@ public class SecurityConfig {
|
||||
.exceptionHandling((exceptions) -> exceptions
|
||||
.authenticationEntryPoint(
|
||||
new LoginUrlAuthenticationEntryPoint("/login"))
|
||||
);
|
||||
)
|
||||
// Accept access tokens for User Info and/or Client Registration
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
@@ -112,6 +119,7 @@ public class SecurityConfig {
|
||||
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
|
||||
.redirectUri("http://127.0.0.1:8080/authorized")
|
||||
.scope(OidcScopes.OPENID)
|
||||
.scope(OidcScopes.PROFILE)
|
||||
.scope("message.read")
|
||||
.scope("message.write")
|
||||
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
|
||||
@@ -150,8 +158,13 @@ public class SecurityConfig {
|
||||
}
|
||||
|
||||
@Bean // <7>
|
||||
public ProviderSettings providerSettings() {
|
||||
return ProviderSettings.builder().build();
|
||||
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
|
||||
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
|
||||
}
|
||||
|
||||
@Bean // <8>
|
||||
public AuthorizationServerSettings authorizationServerSettings() {
|
||||
return AuthorizationServerSettings.builder().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -44,8 +44,9 @@ 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.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
@@ -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>
|
||||
@@ -158,8 +161,8 @@ public class EnableUserInfoSecurityConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ProviderSettings providerSettings() {
|
||||
return ProviderSettings.builder().build();
|
||||
public AuthorizationServerSettings authorizationServerSettings() {
|
||||
return AuthorizationServerSettings.builder().build();
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -49,8 +49,8 @@ import org.springframework.security.oauth2.server.authorization.config.annotatio
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
|
||||
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.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
@@ -182,8 +182,8 @@ public class JwtUserInfoMapperSecurityConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ProviderSettings providerSettings() {
|
||||
return ProviderSettings.builder().build();
|
||||
public AuthorizationServerSettings authorizationServerSettings() {
|
||||
return AuthorizationServerSettings.builder().build();
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
|
||||
@@ -55,4 +55,5 @@ This is a minimal configuration for getting started quickly. To understand what
|
||||
<4> An instance of xref:core-model-components.adoc#registered-client-repository[`RegisteredClientRepository`] for managing clients.
|
||||
<5> An instance of `com.nimbusds.jose.jwk.source.JWKSource` for signing access tokens.
|
||||
<6> An instance of `java.security.KeyPair` with keys generated on startup used to create the `JWKSource` above.
|
||||
<7> An instance of xref:configuration-model#configuring-provider-settings[`ProviderSettings`] to configure Spring Authorization Server.
|
||||
<7> An instance of {spring-security-api-base-url}/org/springframework/security/oauth2/jwt/JwtDecoder.html[`JwtDecoder`] for decoding signed access tokens.
|
||||
<8> An instance of xref:configuration-model#configuring-authorization-server-settings[`AuthorizationServerSettings`] to configure Spring Authorization Server.
|
||||
|
||||
@@ -6,7 +6,7 @@ This site contains reference documentation and how-to guides for Spring Authoriz
|
||||
[[introducing-spring-authorization-server]]
|
||||
== Introducing Spring Authorization Server
|
||||
|
||||
Spring Authorization Server is a framework that provides implementations of the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05[OAuth 2.1] and https://openid.net/specs/openid-connect-core-1_0.html[OpenID Connect 1.0] specifications and other related specifications.
|
||||
Spring Authorization Server is a framework that provides implementations of the https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07[OAuth 2.1] and https://openid.net/specs/openid-connect-core-1_0.html[OpenID Connect 1.0] specifications and other related specifications.
|
||||
It is built on top of https://spring.io/projects/spring-security[Spring Security] to provide a secure, light-weight, and customizable foundation for building OpenID Connect 1.0 Identity Providers and OAuth2 Authorization Server products.
|
||||
|
||||
[[feature-list]]
|
||||
@@ -25,10 +25,10 @@ Spring Authorization Server supports the following features:
|
||||
* Client Credentials
|
||||
* Refresh Token
|
||||
|
|
||||
* The OAuth 2.1 Authorization Framework (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05[draft])
|
||||
** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05#section-4.1[Authorization Code Grant]
|
||||
** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05#section-4.2[Client Credentials Grant]
|
||||
** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05#section-4.3[Refresh Token Grant]
|
||||
* The OAuth 2.1 Authorization Framework (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07[draft])
|
||||
** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-4.1[Authorization Code Grant]
|
||||
** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-4.2[Client Credentials Grant]
|
||||
** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-4.3[Refresh Token Grant]
|
||||
* OpenID Connect Core 1.0 (https://openid.net/specs/openid-connect-core-1_0.html[spec])
|
||||
** https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[Authorization Code Flow]
|
||||
|
||||
@@ -48,7 +48,7 @@ Spring Authorization Server supports the following features:
|
||||
* `private_key_jwt`
|
||||
* `none` (public clients)
|
||||
|
|
||||
* The OAuth 2.1 Authorization Framework (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05#section-2.4[Client Authentication])
|
||||
* The OAuth 2.1 Authorization Framework (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-2.4[Client Authentication])
|
||||
* JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication (https://tools.ietf.org/html/rfc7523[RFC 7523])
|
||||
* Proof Key for Code Exchange by OAuth Public Clients (PKCE) (https://tools.ietf.org/html/rfc7636[RFC 7636])
|
||||
|
||||
@@ -64,9 +64,9 @@ Spring Authorization Server supports the following features:
|
||||
* xref:protocol-endpoints.adoc#oidc-user-info-endpoint[OpenID Connect 1.0 UserInfo Endpoint]
|
||||
* xref:protocol-endpoints.adoc#oidc-client-registration-endpoint[OpenID Connect 1.0 Client Registration Endpoint]
|
||||
|
|
||||
* The OAuth 2.1 Authorization Framework (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05[draft])
|
||||
** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05#section-3.1[Authorization Endpoint]
|
||||
** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-05#section-3.2[Token Endpoint]
|
||||
* The OAuth 2.1 Authorization Framework (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07[draft])
|
||||
** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-3.1[Authorization Endpoint]
|
||||
** https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-3.2[Token Endpoint]
|
||||
* OAuth 2.0 Token Introspection (https://tools.ietf.org/html/rfc7662[RFC 7662])
|
||||
* OAuth 2.0 Token Revocation (https://tools.ietf.org/html/rfc7009[RFC 7009])
|
||||
* OAuth 2.0 Authorization Server Metadata (https://tools.ietf.org/html/rfc8414[RFC 8414])
|
||||
|
||||
@@ -21,31 +21,105 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
.authorizationEndpoint(authorizationEndpoint ->
|
||||
authorizationEndpoint
|
||||
.authorizationRequestConverter(authorizationRequestConverter) <1>
|
||||
.authenticationProvider(authenticationProvider) <2>
|
||||
.authorizationResponseHandler(authorizationResponseHandler) <3>
|
||||
.errorResponseHandler(errorResponseHandler) <4>
|
||||
.consentPage("/oauth2/v1/authorize") <5>
|
||||
.authorizationRequestConverters(authorizationRequestConvertersConsumer) <2>
|
||||
.authenticationProvider(authenticationProvider) <3>
|
||||
.authenticationProviders(authenticationProvidersConsumer) <4>
|
||||
.authorizationResponseHandler(authorizationResponseHandler) <5>
|
||||
.errorResponseHandler(errorResponseHandler) <6>
|
||||
.consentPage("/oauth2/v1/authorize") <7>
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
<1> `authorizationRequestConverter()`: The `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1[OAuth2 authorization request] (or consent) from `HttpServletRequest` to an instance of `OAuth2AuthorizationCodeRequestAuthenticationToken`.
|
||||
<2> `authenticationProvider()`: The `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2AuthorizationCodeRequestAuthenticationToken`. (One or more may be added to replace the defaults.)
|
||||
<3> `authorizationResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2AuthorizationCodeRequestAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2[OAuth2AuthorizationResponse].
|
||||
<4> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthorizationCodeRequestAuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1[OAuth2Error response].
|
||||
<5> `consentPage()`: The `URI` of the custom consent page to redirect resource owners to if consent is required during the authorization request flow.
|
||||
<1> `authorizationRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1[OAuth2 authorization request] (or consent) from `HttpServletRequest` to an instance of `OAuth2AuthorizationCodeRequestAuthenticationToken` or `OAuth2AuthorizationConsentAuthenticationToken`.
|
||||
<2> `authorizationRequestConverters()`: 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 `OAuth2AuthorizationCodeRequestAuthenticationToken` or `OAuth2AuthorizationConsentAuthenticationToken`.
|
||||
<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> `authorizationResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2AuthorizationCodeRequestAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2[OAuth2AuthorizationResponse].
|
||||
<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthorizationCodeRequestAuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1[OAuth2Error response].
|
||||
<7> `consentPage()`: The `URI` of the custom consent page to redirect resource owners to if consent is required during the authorization request flow.
|
||||
|
||||
`OAuth2AuthorizationEndpointConfigurer` configures the `OAuth2AuthorizationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OAuth2AuthorizationEndpointFilter` is the `Filter` that processes OAuth2 authorization requests (and consents).
|
||||
|
||||
`OAuth2AuthorizationEndpointFilter` is configured with the following defaults:
|
||||
|
||||
* `*AuthenticationConverter*` -- An `OAuth2AuthorizationCodeRequestAuthenticationConverter`.
|
||||
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2AuthorizationCodeRequestAuthenticationProvider`.
|
||||
* `*AuthenticationConverter*` -- A `DelegatingAuthenticationConverter` composed of `OAuth2AuthorizationCodeRequestAuthenticationConverter` and `OAuth2AuthorizationConsentAuthenticationConverter`.
|
||||
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2AuthorizationCodeRequestAuthenticationProvider` and `OAuth2AuthorizationConsentAuthenticationProvider`.
|
||||
* `*AuthenticationSuccessHandler*` -- An internal implementation that handles an "`authenticated`" `OAuth2AuthorizationCodeRequestAuthenticationToken` and returns the `OAuth2AuthorizationResponse`.
|
||||
* `*AuthenticationFailureHandler*` -- An internal implementation that uses the `OAuth2Error` associated with the `OAuth2AuthorizationCodeRequestAuthenticationException` and returns the `OAuth2Error` response.
|
||||
|
||||
[[oauth2-authorization-endpoint-customizing-authorization-request-validation]]
|
||||
=== Customizing Authorization Request Validation
|
||||
|
||||
`OAuth2AuthorizationCodeRequestAuthenticationValidator` is the default validator used for validating specific OAuth2 authorization request parameters used in the Authorization Code Grant.
|
||||
The default implementation validates the `redirect_uri` and `scope` parameters.
|
||||
If validation fails, an `OAuth2AuthorizationCodeRequestAuthenticationException` is thrown.
|
||||
|
||||
`OAuth2AuthorizationCodeRequestAuthenticationProvider` provides the ability to override the default authorization request validation by supplying a custom authentication validator of type `Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext>` to `setAuthenticationValidator()`.
|
||||
|
||||
[TIP]
|
||||
`OAuth2AuthorizationCodeRequestAuthenticationContext` holds the `OAuth2AuthorizationCodeRequestAuthenticationToken`, which contains the OAuth2 authorization request parameters.
|
||||
|
||||
[IMPORTANT]
|
||||
If validation fails, the authentication validator *MUST* throw `OAuth2AuthorizationCodeRequestAuthenticationException`.
|
||||
|
||||
A common use case during the development life cycle phase is to allow for `localhost` in the `redirect_uri` parameter.
|
||||
|
||||
The following example shows how to configure `OAuth2AuthorizationCodeRequestAuthenticationProvider` with a custom authentication validator that allows for `localhost` in the `redirect_uri` parameter:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer();
|
||||
http.apply(authorizationServerConfigurer);
|
||||
|
||||
authorizationServerConfigurer
|
||||
.authorizationEndpoint(authorizationEndpoint ->
|
||||
authorizationEndpoint
|
||||
.authenticationProviders(configureAuthenticationValidator())
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private Consumer<List<AuthenticationProvider>> configureAuthenticationValidator() {
|
||||
return (authenticationProviders) ->
|
||||
authenticationProviders.forEach((authenticationProvider) -> {
|
||||
if (authenticationProvider instanceof OAuth2AuthorizationCodeRequestAuthenticationProvider) {
|
||||
Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator =
|
||||
// Override default redirect_uri validator
|
||||
new CustomRedirectUriValidator()
|
||||
// Reuse default scope validator
|
||||
.andThen(OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_SCOPE_VALIDATOR);
|
||||
|
||||
((OAuth2AuthorizationCodeRequestAuthenticationProvider) authenticationProvider)
|
||||
.setAuthenticationValidator(authenticationValidator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static class CustomRedirectUriValidator implements Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> {
|
||||
|
||||
@Override
|
||||
public void accept(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
authenticationContext.getAuthentication();
|
||||
RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
|
||||
String requestedRedirectUri = authorizationCodeRequestAuthentication.getRedirectUri();
|
||||
|
||||
// Use exact string matching when comparing client redirect URIs against pre-registered URIs
|
||||
if (!registeredClient.getRedirectUris().contains(requestedRedirectUri)) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[oauth2-token-endpoint]]
|
||||
== OAuth2 Token Endpoint
|
||||
|
||||
@@ -65,18 +139,22 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
.tokenEndpoint(tokenEndpoint ->
|
||||
tokenEndpoint
|
||||
.accessTokenRequestConverter(accessTokenRequestConverter) <1>
|
||||
.authenticationProvider(authenticationProvider) <2>
|
||||
.accessTokenResponseHandler(accessTokenResponseHandler) <3>
|
||||
.errorResponseHandler(errorResponseHandler) <4>
|
||||
.accessTokenRequestConverters(accessTokenRequestConvertersConsumer) <2>
|
||||
.authenticationProvider(authenticationProvider) <3>
|
||||
.authenticationProviders(authenticationProvidersConsumer) <4>
|
||||
.accessTokenResponseHandler(accessTokenResponseHandler) <5>
|
||||
.errorResponseHandler(errorResponseHandler) <6>
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
<1> `accessTokenRequestConverter()`: The `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3[OAuth2 access token request] from `HttpServletRequest` to an instance of `OAuth2AuthorizationGrantAuthenticationToken`.
|
||||
<2> `authenticationProvider()`: The `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2AuthorizationGrantAuthenticationToken`. (One or more may be added to replace the defaults.)
|
||||
<3> `accessTokenResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an `OAuth2AccessTokenAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.1[`OAuth2AccessTokenResponse`].
|
||||
<4> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.2[OAuth2Error response].
|
||||
<1> `accessTokenRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3[OAuth2 access token request] from `HttpServletRequest` to an instance of `OAuth2AuthorizationGrantAuthenticationToken`.
|
||||
<2> `accessTokenRequestConverters()`: 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 `OAuth2AuthorizationGrantAuthenticationToken`.
|
||||
<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> `accessTokenResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an `OAuth2AccessTokenAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.1[`OAuth2AccessTokenResponse`].
|
||||
<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-5.2[OAuth2Error response].
|
||||
|
||||
`OAuth2TokenEndpointConfigurer` configures the `OAuth2TokenEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OAuth2TokenEndpointFilter` is the `Filter` that processes OAuth2 access token requests.
|
||||
@@ -110,25 +188,29 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint ->
|
||||
tokenIntrospectionEndpoint
|
||||
.introspectionRequestConverter(introspectionRequestConverter) <1>
|
||||
.authenticationProvider(authenticationProvider) <2>
|
||||
.introspectionResponseHandler(introspectionResponseHandler) <3>
|
||||
.errorResponseHandler(errorResponseHandler) <4>
|
||||
.introspectionRequestConverters(introspectionRequestConvertersConsumer) <2>
|
||||
.authenticationProvider(authenticationProvider) <3>
|
||||
.authenticationProviders(authenticationProvidersConsumer) <4>
|
||||
.introspectionResponseHandler(introspectionResponseHandler) <5>
|
||||
.errorResponseHandler(errorResponseHandler) <6>
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
<1> `introspectionRequestConverter()`: The `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc7662#section-2.1[OAuth2 introspection request] from `HttpServletRequest` to an instance of `OAuth2TokenIntrospectionAuthenticationToken`.
|
||||
<2> `authenticationProvider()`: The `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2TokenIntrospectionAuthenticationToken`. (One or more may be added to replace the defaults.)
|
||||
<3> `introspectionResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2TokenIntrospectionAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc7662#section-2.2[OAuth2TokenIntrospection response].
|
||||
<4> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc7662#section-2.3[OAuth2Error response].
|
||||
<1> `introspectionRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc7662#section-2.1[OAuth2 introspection request] from `HttpServletRequest` to an instance of `OAuth2TokenIntrospectionAuthenticationToken`.
|
||||
<2> `introspectionRequestConverters()`: 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 `OAuth2TokenIntrospectionAuthenticationToken`.
|
||||
<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> `introspectionResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2TokenIntrospectionAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc7662#section-2.2[OAuth2TokenIntrospection response].
|
||||
<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc7662#section-2.3[OAuth2Error response].
|
||||
|
||||
`OAuth2TokenIntrospectionEndpointConfigurer` configures the `OAuth2TokenIntrospectionEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OAuth2TokenIntrospectionEndpointFilter` is the `Filter` that processes OAuth2 introspection requests.
|
||||
|
||||
`OAuth2TokenIntrospectionEndpointFilter` is configured with the following defaults:
|
||||
|
||||
* `*AuthenticationConverter*` -- An internal implementation that returns the `OAuth2TokenIntrospectionAuthenticationToken`.
|
||||
* `*AuthenticationConverter*` -- An `OAuth2TokenIntrospectionAuthenticationConverter`.
|
||||
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2TokenIntrospectionAuthenticationProvider`.
|
||||
* `*AuthenticationSuccessHandler*` -- An internal implementation that handles an "`authenticated`" `OAuth2TokenIntrospectionAuthenticationToken` and returns the `OAuth2TokenIntrospection` response.
|
||||
* `*AuthenticationFailureHandler*` -- An internal implementation that uses the `OAuth2Error` associated with the `OAuth2AuthenticationException` and returns the `OAuth2Error` response.
|
||||
@@ -152,26 +234,30 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
authorizationServerConfigurer
|
||||
.tokenRevocationEndpoint(tokenRevocationEndpoint ->
|
||||
tokenRevocationEndpoint
|
||||
.revocationRequestConverter(revocationRequestConverter) <1>
|
||||
.authenticationProvider(authenticationProvider) <2>
|
||||
.revocationResponseHandler(revocationResponseHandler) <3>
|
||||
.errorResponseHandler(errorResponseHandler) <4>
|
||||
.revocationRequestConverter(revocationRequestConverter) <1>
|
||||
.revocationRequestConverters(revocationRequestConvertersConsumer) <2>
|
||||
.authenticationProvider(authenticationProvider) <3>
|
||||
.authenticationProviders(authenticationProvidersConsumer) <4>
|
||||
.revocationResponseHandler(revocationResponseHandler) <5>
|
||||
.errorResponseHandler(errorResponseHandler) <6>
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
<1> `revocationRequestConverter()`: The `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc7009#section-2.1[OAuth2 revocation request] from `HttpServletRequest` to an instance of `OAuth2TokenRevocationAuthenticationToken`.
|
||||
<2> `authenticationProvider()`: The `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2TokenRevocationAuthenticationToken`. (One or more may be added to replace the defaults.)
|
||||
<3> `revocationResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2TokenRevocationAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc7009#section-2.2[OAuth2 revocation response].
|
||||
<4> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc7009#section-2.2.1[OAuth2Error response].
|
||||
<1> `revocationRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc7009#section-2.1[OAuth2 revocation request] from `HttpServletRequest` to an instance of `OAuth2TokenRevocationAuthenticationToken`.
|
||||
<2> `revocationRequestConverters()`: 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 `OAuth2TokenRevocationAuthenticationToken`.
|
||||
<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> `revocationResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2TokenRevocationAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc7009#section-2.2[OAuth2 revocation response].
|
||||
<6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc7009#section-2.2.1[OAuth2Error response].
|
||||
|
||||
`OAuth2TokenRevocationEndpointConfigurer` configures the `OAuth2TokenRevocationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OAuth2TokenRevocationEndpointFilter` is the `Filter` that processes OAuth2 revocation requests.
|
||||
|
||||
`OAuth2TokenRevocationEndpointFilter` is configured with the following defaults:
|
||||
|
||||
* `*AuthenticationConverter*` -- An internal implementation that returns the `OAuth2TokenRevocationAuthenticationToken`.
|
||||
* `*AuthenticationConverter*` -- An `OAuth2TokenRevocationAuthenticationConverter`.
|
||||
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2TokenRevocationAuthenticationProvider`.
|
||||
* `*AuthenticationSuccessHandler*` -- An internal implementation that handles an "`authenticated`" `OAuth2TokenRevocationAuthenticationToken` and returns the OAuth2 revocation response.
|
||||
* `*AuthenticationFailureHandler*` -- An internal implementation that uses the `OAuth2Error` associated with the `OAuth2AuthenticationException` and returns the `OAuth2Error` response.
|
||||
@@ -179,10 +265,31 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
[[oauth2-authorization-server-metadata-endpoint]]
|
||||
== OAuth2 Authorization Server Metadata Endpoint
|
||||
|
||||
`OAuth2AuthorizationServerConfigurer` provides support for the https://datatracker.ietf.org/doc/html/rfc8414#section-3[OAuth2 Authorization Server Metadata endpoint].
|
||||
`OAuth2AuthorizationServerMetadataEndpointConfigurer` provides the ability to customize the https://datatracker.ietf.org/doc/html/rfc8414#section-3[OAuth2 Authorization Server Metadata endpoint].
|
||||
It defines an extension point that lets you customize the https://datatracker.ietf.org/doc/html/rfc8414#section-3.2[OAuth2 Authorization Server Metadata response].
|
||||
|
||||
`OAuth2AuthorizationServerConfigurer` configures the `OAuth2AuthorizationServerMetadataEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OAuth2AuthorizationServerMetadataEndpointFilter` is the `Filter` that processes https://datatracker.ietf.org/doc/html/rfc8414#section-3.1[OAuth2 authorization server metadata requests] and returns the https://datatracker.ietf.org/doc/html/rfc8414#section-3.2[OAuth2AuthorizationServerMetadata response].
|
||||
`OAuth2AuthorizationServerMetadataEndpointConfigurer` provides the following configuration option:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer();
|
||||
http.apply(authorizationServerConfigurer);
|
||||
|
||||
authorizationServerConfigurer
|
||||
.authorizationServerMetadataEndpoint(authorizationServerMetadataEndpoint ->
|
||||
authorizationServerMetadataEndpoint
|
||||
.authorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer)); <1>
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
<1> `authorizationServerMetadataCustomizer()`: The `Consumer` providing access to the `OAuth2AuthorizationServerMetadata.Builder` allowing the ability to customize the claims of the Authorization Server's configuration.
|
||||
|
||||
`OAuth2AuthorizationServerMetadataEndpointConfigurer` configures the `OAuth2AuthorizationServerMetadataEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OAuth2AuthorizationServerMetadataEndpointFilter` is the `Filter` that returns the https://datatracker.ietf.org/doc/html/rfc8414#section-3.2[OAuth2AuthorizationServerMetadata response].
|
||||
|
||||
[[jwk-set-endpoint]]
|
||||
== JWK Set Endpoint
|
||||
@@ -198,18 +305,43 @@ The JWK Set endpoint is configured *only* if a `JWKSource<SecurityContext>` `@Be
|
||||
[[oidc-provider-configuration-endpoint]]
|
||||
== OpenID Connect 1.0 Provider Configuration Endpoint
|
||||
|
||||
`OidcConfigurer` provides support for the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Connect 1.0 Provider Configuration endpoint].
|
||||
`OidcProviderConfigurationEndpointConfigurer` provides the ability to customize the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Connect 1.0 Provider Configuration endpoint].
|
||||
It defines an extension point that lets you customize the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse[OpenID Provider Configuration response].
|
||||
|
||||
`OidcConfigurer` configures the `OidcProviderConfigurationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OidcProviderConfigurationEndpointConfigurer` provides the following configuration option:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer();
|
||||
http.apply(authorizationServerConfigurer);
|
||||
|
||||
authorizationServerConfigurer
|
||||
.oidc(oidc ->
|
||||
oidc
|
||||
.providerConfigurationEndpoint(providerConfigurationEndpoint ->
|
||||
providerConfigurationEndpoint
|
||||
.providerConfigurationCustomizer(providerConfigurationCustomizer) <1>
|
||||
)
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
<1> `providerConfigurationCustomizer()`: The `Consumer` providing access to the `OidcProviderConfiguration.Builder` allowing the ability to customize the claims of the OpenID Provider's configuration.
|
||||
|
||||
`OidcProviderConfigurationEndpointConfigurer` configures the `OidcProviderConfigurationEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
|
||||
`OidcProviderConfigurationEndpointFilter` is the `Filter` that returns the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse[OidcProviderConfiguration response].
|
||||
|
||||
[[oidc-user-info-endpoint]]
|
||||
== 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]
|
||||
----
|
||||
@@ -223,21 +355,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`.
|
||||
@@ -275,8 +423,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]
|
||||
----
|
||||
@@ -289,12 +439,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.
|
||||
@@ -307,7 +471,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-M1
|
||||
version=0.4.1-SNAPSHOT
|
||||
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.24
|
||||
springSecurityVersion=5.8.0
|
||||
springJavaformatVersion=0.0.31
|
||||
springJavaformatExcludePackages=org/springframework/security/config org/springframework/security/oauth2
|
||||
checkstyleToolVersion=8.34
|
||||
|
||||
@@ -21,7 +21,7 @@ dependencies {
|
||||
|
||||
testImplementation "org.springframework.security:spring-security-test"
|
||||
testImplementation "org.springframework:spring-webmvc"
|
||||
testImplementation "junit:junit"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter"
|
||||
testImplementation "org.assertj:assertj-core"
|
||||
testImplementation "org.mockito:mockito-core"
|
||||
testImplementation "com.jayway.jsonpath:json-path"
|
||||
|
||||
@@ -274,6 +274,17 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth
|
||||
return getThis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@code registration_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
|
||||
*
|
||||
* @param clientRegistrationEndpoint the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public B clientRegistrationEndpoint(String clientRegistrationEndpoint) {
|
||||
return claim(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT, clientRegistrationEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this Proof Key for Code Exchange (PKCE) {@code code_challenge_method} to the collection of {@code code_challenge_methods_supported}
|
||||
* in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
|
||||
@@ -369,6 +380,9 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth
|
||||
Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenIntrospectionEndpointAuthenticationMethods must be of type List");
|
||||
Assert.notEmpty((List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenIntrospectionEndpointAuthenticationMethods cannot be empty");
|
||||
}
|
||||
if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT) != null) {
|
||||
validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT), "clientRegistrationEndpoint must be a valid URL");
|
||||
}
|
||||
if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED) != null) {
|
||||
Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED), "codeChallengeMethods must be of type List");
|
||||
Assert.notEmpty((List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED), "codeChallengeMethods cannot be empty");
|
||||
|
||||
@@ -141,6 +141,16 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc
|
||||
return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint {@code (registration_endpoint)}.
|
||||
*
|
||||
* @return the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint
|
||||
* @since 0.4.0
|
||||
*/
|
||||
default URL getClientRegistrationEndpoint() {
|
||||
return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported {@code (code_challenge_methods_supported)}.
|
||||
*
|
||||
|
||||
@@ -86,6 +86,12 @@ public class OAuth2AuthorizationServerMetadataClaimNames {
|
||||
*/
|
||||
public static final String INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED = "introspection_endpoint_auth_methods_supported";
|
||||
|
||||
/**
|
||||
* {@code registration_endpoint} - the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public static final String REGISTRATION_ENDPOINT = "registration_endpoint";
|
||||
|
||||
/**
|
||||
* {@code code_challenge_methods_supported} - the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
@@ -45,6 +50,7 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public final class ClientSecretAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final CodeVerifierAuthenticator codeVerifierAuthenticator;
|
||||
private PasswordEncoder passwordEncoder;
|
||||
@@ -93,6 +99,10 @@ public final class ClientSecretAuthenticationProvider implements AuthenticationP
|
||||
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved registered client");
|
||||
}
|
||||
|
||||
if (!registeredClient.getClientAuthenticationMethods().contains(
|
||||
clientAuthentication.getClientAuthenticationMethod())) {
|
||||
throwInvalidClient("authentication_method");
|
||||
@@ -107,9 +117,22 @@ public final class ClientSecretAuthenticationProvider implements AuthenticationP
|
||||
throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
|
||||
}
|
||||
|
||||
if (registeredClient.getClientSecretExpiresAt() != null &&
|
||||
Instant.now().isAfter(registeredClient.getClientSecretExpiresAt())) {
|
||||
throwInvalidClient("client_secret_expires_at");
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated client authentication parameters");
|
||||
}
|
||||
|
||||
// Validate the "code_verifier" parameter for the confidential client, if available
|
||||
this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated client secret");
|
||||
}
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(registeredClient,
|
||||
clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
@@ -47,6 +50,7 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
final class CodeVerifierAuthenticator {
|
||||
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
CodeVerifierAuthenticator(OAuth2AuthorizationService authorizationService) {
|
||||
@@ -81,6 +85,10 @@ final class CodeVerifierAuthenticator {
|
||||
throwInvalidGrant(OAuth2ParameterNames.CODE);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved authorization with authorization code");
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
|
||||
OAuth2AuthorizationRequest.class.getName());
|
||||
|
||||
@@ -90,10 +98,17 @@ final class CodeVerifierAuthenticator {
|
||||
if (registeredClient.getClientSettings().isRequireProofKey()) {
|
||||
throwInvalidGrant(PkceParameterNames.CODE_CHALLENGE);
|
||||
} else {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Did not authenticate code verifier since requireProofKey=false");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated code verifier parameters");
|
||||
}
|
||||
|
||||
String codeChallengeMethod = (String) authorizationRequest.getAdditionalParameters()
|
||||
.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
|
||||
String codeVerifier = (String) parameters.get(PkceParameterNames.CODE_VERIFIER);
|
||||
@@ -101,6 +116,10 @@ final class CodeVerifierAuthenticator {
|
||||
throwInvalidGrant(PkceParameterNames.CODE_VERIFIER);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated code verifier");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,53 +15,30 @@
|
||||
*/
|
||||
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.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
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.ProviderContext;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
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,14 +47,16 @@ 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";
|
||||
private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD =
|
||||
new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
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 +70,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
|
||||
@@ -109,6 +88,10 @@ public final class JwtClientAssertionAuthenticationProvider implements Authentic
|
||||
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved registered client");
|
||||
}
|
||||
|
||||
if (!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT) &&
|
||||
!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
|
||||
throwInvalidClient("authentication_method");
|
||||
@@ -119,13 +102,17 @@ 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) {
|
||||
throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION, ex);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated client authentication parameters");
|
||||
}
|
||||
|
||||
// Validate the "code_verifier" parameter for the confidential client, if available
|
||||
this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
|
||||
|
||||
@@ -134,6 +121,10 @@ public final class JwtClientAssertionAuthenticationProvider implements Authentic
|
||||
ClientAuthenticationMethod.PRIVATE_KEY_JWT :
|
||||
ClientAuthenticationMethod.CLIENT_SECRET_JWT;
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated client assertion");
|
||||
}
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(registeredClient, clientAuthenticationMethod, jwtAssertion);
|
||||
}
|
||||
|
||||
@@ -142,6 +133,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 +159,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, containsProviderAudience()),
|
||||
new JwtClaimValidator<>(JwtClaimNames.EXP, Objects::nonNull),
|
||||
new JwtTimestampValidator()
|
||||
);
|
||||
}
|
||||
|
||||
private static Predicate<List<String>> containsProviderAudience() {
|
||||
return (audienceClaim) -> {
|
||||
if (CollectionUtils.isEmpty(audienceClaim)) {
|
||||
return false;
|
||||
}
|
||||
List<String> providerAudience = getProviderAudience();
|
||||
for (String audience : audienceClaim) {
|
||||
if (providerAudience.contains(audience)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
private static List<String> getProviderAudience() {
|
||||
ProviderContext providerContext = ProviderContextHolder.getProviderContext();
|
||||
if (!StringUtils.hasText(providerContext.getIssuer())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
ProviderSettings providerSettings = providerContext.getProviderSettings();
|
||||
List<String> providerAudience = new ArrayList<>();
|
||||
providerAudience.add(providerContext.getIssuer());
|
||||
providerAudience.add(asUrl(providerContext.getIssuer(), providerSettings.getTokenEndpoint()));
|
||||
providerAudience.add(asUrl(providerContext.getIssuer(), providerSettings.getTokenIntrospectionEndpoint()));
|
||||
providerAudience.add(asUrl(providerContext.getIssuer(), providerSettings.getTokenRevocationEndpoint()));
|
||||
return providerAudience;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,54 +15,24 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.server.authorization.context.Context;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* A context that holds an {@link Authentication} and (optionally) additional information.
|
||||
* A context that holds an {@link Authentication} and (optionally) additional information
|
||||
* and is used in an {@link AuthenticationProvider}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.0
|
||||
* @see Context
|
||||
*/
|
||||
public class OAuth2AuthenticationContext implements Context {
|
||||
private final Map<Object, Object> context;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthenticationContext} using the provided parameters.
|
||||
*
|
||||
* @param authentication the {@code Authentication}
|
||||
* @param context a {@code Map} of additional context information
|
||||
*/
|
||||
public OAuth2AuthenticationContext(Authentication authentication, @Nullable Map<Object, Object> context) {
|
||||
Assert.notNull(authentication, "authentication cannot be null");
|
||||
Map<Object, Object> ctx = new HashMap<>();
|
||||
if (!CollectionUtils.isEmpty(context)) {
|
||||
ctx.putAll(context);
|
||||
}
|
||||
ctx.put(Authentication.class, authentication);
|
||||
this.context = Collections.unmodifiableMap(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthenticationContext} using the provided parameters.
|
||||
*
|
||||
* @param context a {@code Map} of context information, must contain the {@code Authentication}
|
||||
* @since 0.2.1
|
||||
*/
|
||||
public OAuth2AuthenticationContext(Map<Object, Object> context) {
|
||||
Assert.notEmpty(context, "context cannot be empty");
|
||||
Assert.notNull(context.get(Authentication.class), "authentication cannot be null");
|
||||
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||
}
|
||||
public interface OAuth2AuthenticationContext extends Context {
|
||||
|
||||
/**
|
||||
* Returns the {@link Authentication} associated to the context.
|
||||
@@ -71,23 +41,10 @@ public class OAuth2AuthenticationContext implements Context {
|
||||
* @return the {@link Authentication}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Authentication> T getAuthentication() {
|
||||
default <T extends Authentication> T getAuthentication() {
|
||||
return (T) get(Authentication.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
return this.context.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for subclasses of {@link OAuth2AuthenticationContext}.
|
||||
*
|
||||
@@ -95,7 +52,7 @@ public class OAuth2AuthenticationContext implements Context {
|
||||
* @param <B> the type of the builder
|
||||
* @since 0.2.1
|
||||
*/
|
||||
protected static abstract class AbstractBuilder<T extends OAuth2AuthenticationContext, B extends AbstractBuilder<T, B>> {
|
||||
abstract class AbstractBuilder<T extends OAuth2AuthenticationContext, B extends AbstractBuilder<T, B>> {
|
||||
private final Map<Object, Object> context = new HashMap<>();
|
||||
|
||||
protected AbstractBuilder(Authentication authentication) {
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* 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.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for validating the attribute(s)
|
||||
* of the {@link Authentication} associated to the {@link OAuth2AuthenticationContext}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.0
|
||||
* @see OAuth2AuthenticationContext
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface OAuth2AuthenticationValidator {
|
||||
|
||||
/**
|
||||
* Validate the attribute(s) of the {@link Authentication}.
|
||||
*
|
||||
* @param authenticationContext the authentication context
|
||||
* @throws OAuth2AuthenticationException if the attribute(s) of the {@code Authentication} is invalid
|
||||
*/
|
||||
void validate(OAuth2AuthenticationContext authenticationContext) throws OAuth2AuthenticationException;
|
||||
|
||||
}
|
||||
@@ -20,6 +20,10 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
@@ -43,7 +47,7 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
|
||||
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.context.ProviderContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
@@ -72,6 +76,7 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
|
||||
new OAuth2TokenType(OAuth2ParameterNames.CODE);
|
||||
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE =
|
||||
new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
|
||||
@@ -99,11 +104,20 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
|
||||
getAuthenticatedClientElseThrowInvalidClient(authorizationCodeAuthentication);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved registered client");
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
authorizationCodeAuthentication.getCode(), AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
if (authorization == null) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved authorization with authorization code");
|
||||
}
|
||||
|
||||
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
|
||||
authorization.getToken(OAuth2AuthorizationCode.class);
|
||||
|
||||
@@ -115,6 +129,9 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
|
||||
// Invalidate the authorization code given that a different client is attempting to use it
|
||||
authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken());
|
||||
this.authorizationService.save(authorization);
|
||||
if (this.logger.isWarnEnabled()) {
|
||||
this.logger.warn(LogMessage.format("Invalidated authorization code used by registered client '%s'", registeredClient.getId()));
|
||||
}
|
||||
}
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
@@ -128,11 +145,15 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated token request parameters");
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(authorization.getAttribute(Principal.class.getName()))
|
||||
.providerContext(ProviderContextHolder.getProviderContext())
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.authorization(authorization)
|
||||
.authorizedScopes(authorization.getAuthorizedScopes())
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
@@ -149,6 +170,11 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
|
||||
"The token generator failed to generate the access token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Generated access token");
|
||||
}
|
||||
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
@@ -172,6 +198,11 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
|
||||
"The token generator failed to generate the refresh token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Generated refresh token");
|
||||
}
|
||||
|
||||
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||
authorizationBuilder.refreshToken(refreshToken);
|
||||
}
|
||||
@@ -191,6 +222,11 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
|
||||
"The token generator failed to generate the ID token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Generated id token");
|
||||
}
|
||||
|
||||
idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
|
||||
generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
|
||||
authorizationBuilder.token(idToken, (metadata) ->
|
||||
@@ -206,12 +242,20 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
|
||||
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Saved authorization");
|
||||
}
|
||||
|
||||
Map<String, Object> additionalParameters = Collections.emptyMap();
|
||||
if (idToken != null) {
|
||||
additionalParameters = new HashMap<>();
|
||||
additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated token request");
|
||||
}
|
||||
|
||||
return new OAuth2AccessTokenAuthenticationToken(
|
||||
registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.time.Instant;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2TokenGenerator} that generates an {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see OAuth2TokenGenerator
|
||||
* @see OAuth2AuthorizationCode
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider
|
||||
*/
|
||||
final class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator<OAuth2AuthorizationCode> {
|
||||
private final StringKeyGenerator authorizationCodeGenerator =
|
||||
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public OAuth2AuthorizationCode generate(OAuth2TokenContext context) {
|
||||
if (context.getTokenType() == null ||
|
||||
!OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) {
|
||||
return null;
|
||||
}
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getAuthorizationCodeTimeToLive());
|
||||
return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2AuthenticationContext} that holds an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} and additional information
|
||||
* and is used when validating the OAuth 2.0 Authorization Request used in the Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see OAuth2AuthenticationContext
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
*/
|
||||
public final class OAuth2AuthorizationCodeRequestAuthenticationContext implements OAuth2AuthenticationContext {
|
||||
private final Map<Object, Object> context;
|
||||
|
||||
private OAuth2AuthorizationCodeRequestAuthenticationContext(Map<Object, Object> context) {
|
||||
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
return this.context.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link RegisteredClient registered client}.
|
||||
*
|
||||
* @return the {@link RegisteredClient}
|
||||
*/
|
||||
public RegisteredClient getRegisteredClient() {
|
||||
return get(RegisteredClient.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
*
|
||||
* @param authentication the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
|
||||
return new Builder(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OAuth2AuthorizationCodeRequestAuthenticationContext}.
|
||||
*/
|
||||
public static final class Builder extends AbstractBuilder<OAuth2AuthorizationCodeRequestAuthenticationContext, Builder> {
|
||||
|
||||
private Builder(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
|
||||
super(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RegisteredClient registered client}.
|
||||
*
|
||||
* @param registeredClient the {@link RegisteredClient}
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder registeredClient(RegisteredClient registeredClient) {
|
||||
return put(RegisteredClient.class, registeredClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OAuth2AuthorizationCodeRequestAuthenticationContext}.
|
||||
*
|
||||
* @return the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
|
||||
*/
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationContext build() {
|
||||
Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null");
|
||||
return new OAuth2AuthorizationCodeRequestAuthenticationContext(getContext());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,22 +16,17 @@
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
@@ -49,24 +44,24 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
|
||||
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.context.ProviderContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request (and Consent)
|
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request
|
||||
* used in the Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.1.2
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationValidator
|
||||
* @see OAuth2AuthorizationCodeAuthenticationProvider
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OAuth2AuthorizationConsentService
|
||||
@@ -75,17 +70,15 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
|
||||
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
|
||||
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
|
||||
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
|
||||
new Base64StringKeyGenerator(Base64.getUrlEncoder());
|
||||
private static final Function<String, OAuth2AuthenticationValidator> DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER =
|
||||
createDefaultAuthenticationValidatorResolver();
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2AuthorizationConsentService authorizationConsentService;
|
||||
private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
|
||||
private Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER;
|
||||
private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
|
||||
private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationValidator();
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using the provided parameters.
|
||||
@@ -109,76 +102,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
||||
|
||||
return authorizationCodeRequestAuthentication.isConsent() ?
|
||||
authenticateAuthorizationConsent(authentication) :
|
||||
authenticateAuthorizationRequest(authentication);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
|
||||
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
|
||||
this.authorizationCodeGenerator = authorizationCodeGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter.
|
||||
*
|
||||
* <p>
|
||||
* The following OAuth 2.0 Authorization Request parameters are supported:
|
||||
* <ol>
|
||||
* <li>{@link OAuth2ParameterNames#REDIRECT_URI}</li>
|
||||
* <li>{@link OAuth2ParameterNames#SCOPE}</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The resolved {@link OAuth2AuthenticationValidator} MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
|
||||
*
|
||||
* @param authenticationValidatorResolver the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter
|
||||
*/
|
||||
public void setAuthenticationValidatorResolver(Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver) {
|
||||
Assert.notNull(authenticationValidatorResolver, "authenticationValidatorResolver cannot be null");
|
||||
this.authenticationValidatorResolver = authenticationValidatorResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationConsentAuthenticationContext}
|
||||
* containing an {@link OAuth2AuthorizationConsent.Builder} and additional context information.
|
||||
*
|
||||
* <p>
|
||||
* The following context attributes are available:
|
||||
* <ul>
|
||||
* <li>The {@link OAuth2AuthorizationConsent.Builder} used to build the authorization consent
|
||||
* prior to {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}.</li>
|
||||
* <li>The {@link Authentication} of type
|
||||
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.</li>
|
||||
* <li>The {@link RegisteredClient} associated with the authorization request.</li>
|
||||
* <li>The {@link OAuth2Authorization} associated with the state token presented in the
|
||||
* authorization consent request.</li>
|
||||
* <li>The {@link OAuth2AuthorizationRequest} associated with the authorization consent request.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param authorizationConsentCustomizer the {@code Consumer} providing access to the
|
||||
* {@link OAuth2AuthorizationConsentAuthenticationContext} containing an {@link OAuth2AuthorizationConsent.Builder}
|
||||
*/
|
||||
public void setAuthorizationConsentCustomizer(Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer) {
|
||||
Assert.notNull(authorizationConsentCustomizer, "authorizationConsentCustomizer cannot be null");
|
||||
this.authorizationConsentCustomizer = authorizationConsentCustomizer;
|
||||
}
|
||||
|
||||
private Authentication authenticateAuthorizationRequest(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
authorizationCodeRequestAuthentication.getClientId());
|
||||
if (registeredClient == null) {
|
||||
@@ -186,22 +109,21 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
authorizationCodeRequestAuthentication, null);
|
||||
}
|
||||
|
||||
Map<Object, Object> context = new HashMap<>();
|
||||
context.put(RegisteredClient.class, registeredClient);
|
||||
OAuth2AuthenticationContext authenticationContext = new OAuth2AuthenticationContext(
|
||||
authorizationCodeRequestAuthentication, context);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved registered client");
|
||||
}
|
||||
|
||||
OAuth2AuthenticationValidator redirectUriValidator = resolveAuthenticationValidator(OAuth2ParameterNames.REDIRECT_URI);
|
||||
redirectUriValidator.validate(authenticationContext);
|
||||
OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext =
|
||||
OAuth2AuthorizationCodeRequestAuthenticationContext.with(authorizationCodeRequestAuthentication)
|
||||
.registeredClient(registeredClient)
|
||||
.build();
|
||||
this.authenticationValidator.accept(authenticationContext);
|
||||
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
|
||||
throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
|
||||
OAuth2AuthenticationValidator scopeValidator = resolveAuthenticationValidator(OAuth2ParameterNames.SCOPE);
|
||||
scopeValidator.validate(authenticationContext);
|
||||
|
||||
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
|
||||
String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE);
|
||||
if (StringUtils.hasText(codeChallenge)) {
|
||||
@@ -215,12 +137,19 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
authorizationCodeRequestAuthentication, registeredClient, null);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated authorization code request parameters");
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// The request is valid - ensure the resource owner is authenticated
|
||||
// ---------------
|
||||
|
||||
Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
|
||||
if (!isPrincipalAuthenticated(principal)) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Did not authenticate authorization code request since principal not authenticated");
|
||||
}
|
||||
// Return the authorization request as-is where isAuthenticated() is false
|
||||
return authorizationCodeRequestAuthentication;
|
||||
}
|
||||
@@ -242,17 +171,22 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
|
||||
.attribute(OAuth2ParameterNames.STATE, state)
|
||||
.build();
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
logger.trace("Generated authorization consent state");
|
||||
}
|
||||
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
|
||||
currentAuthorizationConsent.getScopes() : null;
|
||||
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
|
||||
.authorizationUri(authorizationRequest.getAuthorizationUri())
|
||||
.scopes(currentAuthorizedScopes)
|
||||
.state(state)
|
||||
.consentRequired(true)
|
||||
.build();
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Saved authorization");
|
||||
}
|
||||
|
||||
return new OAuth2AuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),
|
||||
registeredClient.getClientId(), principal, state, currentAuthorizedScopes, null);
|
||||
}
|
||||
|
||||
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
|
||||
@@ -264,161 +198,65 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Generated authorization code");
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
|
||||
.authorizedScopes(authorizationRequest.getScopes())
|
||||
.token(authorizationCode)
|
||||
.build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
String redirectUri = authorizationRequest.getRedirectUri();
|
||||
if (!StringUtils.hasText(redirectUri)) {
|
||||
redirectUri = registeredClient.getRedirectUris().iterator().next();
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Saved authorization");
|
||||
}
|
||||
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
|
||||
.authorizationUri(authorizationRequest.getAuthorizationUri())
|
||||
.redirectUri(redirectUri)
|
||||
.scopes(authorizationRequest.getScopes())
|
||||
.state(authorizationRequest.getState())
|
||||
.authorizationCode(authorizationCode)
|
||||
.build();
|
||||
}
|
||||
|
||||
private OAuth2AuthenticationValidator resolveAuthenticationValidator(String parameterName) {
|
||||
OAuth2AuthenticationValidator authenticationValidator = this.authenticationValidatorResolver.apply(parameterName);
|
||||
return authenticationValidator != null ?
|
||||
authenticationValidator :
|
||||
DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER.apply(parameterName);
|
||||
}
|
||||
|
||||
private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
authorizationCodeRequestAuthentication.getState(), STATE_TOKEN_TYPE);
|
||||
if (authorization == null) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
|
||||
authorizationCodeRequestAuthentication, null, null);
|
||||
}
|
||||
|
||||
// The 'in-flight' authorization must be associated to the current principal
|
||||
Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
|
||||
if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
|
||||
authorizationCodeRequestAuthentication, null, null);
|
||||
}
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
authorizationCodeRequestAuthentication.getClientId());
|
||||
if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
Set<String> requestedScopes = authorizationRequest.getScopes();
|
||||
Set<String> authorizedScopes = new HashSet<>(authorizationCodeRequestAuthentication.getScopes());
|
||||
if (!requestedScopes.containsAll(authorizedScopes)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
|
||||
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName());
|
||||
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
|
||||
currentAuthorizationConsent.getScopes() : Collections.emptySet();
|
||||
|
||||
if (!currentAuthorizedScopes.isEmpty()) {
|
||||
for (String requestedScope : requestedScopes) {
|
||||
if (currentAuthorizedScopes.contains(requestedScope)) {
|
||||
authorizedScopes.add(requestedScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!authorizedScopes.isEmpty() && requestedScopes.contains(OidcScopes.OPENID)) {
|
||||
// 'openid' scope is auto-approved as it does not require consent
|
||||
authorizedScopes.add(OidcScopes.OPENID);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
|
||||
if (currentAuthorizationConsent != null) {
|
||||
authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
|
||||
} else {
|
||||
authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName());
|
||||
}
|
||||
authorizedScopes.forEach(authorizationConsentBuilder::scope);
|
||||
|
||||
if (this.authorizationConsentCustomizer != null) {
|
||||
// @formatter:off
|
||||
OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =
|
||||
OAuth2AuthorizationConsentAuthenticationContext.with(authorizationCodeRequestAuthentication)
|
||||
.authorizationConsent(authorizationConsentBuilder)
|
||||
.registeredClient(registeredClient)
|
||||
.authorization(authorization)
|
||||
.authorizationRequest(authorizationRequest)
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
|
||||
}
|
||||
|
||||
Set<GrantedAuthority> authorities = new HashSet<>();
|
||||
authorizationConsentBuilder.authorities(authorities::addAll);
|
||||
|
||||
if (authorities.isEmpty()) {
|
||||
// Authorization consent denied (or revoked)
|
||||
if (currentAuthorizationConsent != null) {
|
||||
this.authorizationConsentService.remove(currentAuthorizationConsent);
|
||||
}
|
||||
this.authorizationService.remove(authorization);
|
||||
throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
|
||||
if (!authorizationConsent.equals(currentAuthorizationConsent)) {
|
||||
this.authorizationConsentService.save(authorizationConsent);
|
||||
}
|
||||
|
||||
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
|
||||
authorizationCodeRequestAuthentication, registeredClient, authorization, authorizedScopes);
|
||||
OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
|
||||
if (authorizationCode == null) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the authorization code.", ERROR_URI);
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
|
||||
}
|
||||
|
||||
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.token(authorizationCode)
|
||||
.attributes(attrs -> {
|
||||
attrs.remove(OAuth2ParameterNames.STATE);
|
||||
})
|
||||
.build();
|
||||
this.authorizationService.save(updatedAuthorization);
|
||||
|
||||
String redirectUri = authorizationRequest.getRedirectUri();
|
||||
if (!StringUtils.hasText(redirectUri)) {
|
||||
redirectUri = registeredClient.getRedirectUris().iterator().next();
|
||||
}
|
||||
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
|
||||
.authorizationUri(authorizationRequest.getAuthorizationUri())
|
||||
.redirectUri(redirectUri)
|
||||
.scopes(authorizedScopes)
|
||||
.state(authorizationRequest.getState())
|
||||
.authorizationCode(authorizationCode)
|
||||
.build();
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated authorization code request");
|
||||
}
|
||||
|
||||
return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(),
|
||||
registeredClient.getClientId(), principal, authorizationCode, redirectUri,
|
||||
authorizationRequest.getState(), authorizationRequest.getScopes());
|
||||
}
|
||||
|
||||
private static Function<String, OAuth2AuthenticationValidator> createDefaultAuthenticationValidatorResolver() {
|
||||
Map<String, OAuth2AuthenticationValidator> authenticationValidators = new HashMap<>();
|
||||
authenticationValidators.put(OAuth2ParameterNames.REDIRECT_URI, new DefaultRedirectUriOAuth2AuthenticationValidator());
|
||||
authenticationValidators.put(OAuth2ParameterNames.SCOPE, new DefaultScopeOAuth2AuthenticationValidator());
|
||||
return authenticationValidators::get;
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
|
||||
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
|
||||
this.authorizationCodeGenerator = authorizationCodeGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
|
||||
* and is responsible for validating specific OAuth 2.0 Authorization Request parameters
|
||||
* associated in the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
* The default authentication validator is {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The authentication validator MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
|
||||
*
|
||||
* @param authenticationValidator the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for validating specific OAuth 2.0 Authorization Request parameters
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public void setAuthenticationValidator(Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
|
||||
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
|
||||
this.authenticationValidator = authenticationValidator;
|
||||
}
|
||||
|
||||
private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal,
|
||||
@@ -438,7 +276,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal((Authentication) authorizationCodeRequestAuthentication.getPrincipal())
|
||||
.providerContext(ProviderContextHolder.getProviderContext())
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.tokenType(new OAuth2TokenType(OAuth2ParameterNames.CODE))
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
@@ -481,14 +319,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
private static void throwError(String errorCode, String parameterName,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient) {
|
||||
throwError(errorCode, parameterName, authorizationCodeRequestAuthentication, registeredClient, null);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
|
||||
throwError(errorCode, parameterName, ERROR_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
|
||||
throwError(errorCode, parameterName, ERROR_URI, authorizationCodeRequestAuthentication, registeredClient, null);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName, String errorUri,
|
||||
@@ -502,31 +333,19 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
|
||||
|
||||
boolean redirectOnError = true;
|
||||
String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
|
||||
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
|
||||
(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
|
||||
parameterName.equals(OAuth2ParameterNames.REDIRECT_URI) ||
|
||||
parameterName.equals(OAuth2ParameterNames.STATE))) {
|
||||
redirectOnError = false;
|
||||
redirectUri = null; // Prevent redirects
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = authorizationCodeRequestAuthentication;
|
||||
|
||||
if (redirectOnError && !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
|
||||
String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
|
||||
String state = authorizationCodeRequestAuthentication.isConsent() && authorizationRequest != null ?
|
||||
authorizationRequest.getState() : authorizationCodeRequestAuthentication.getState();
|
||||
authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
|
||||
.redirectUri(redirectUri)
|
||||
.state(state)
|
||||
.build();
|
||||
authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
|
||||
} else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
|
||||
authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
|
||||
.redirectUri(null) // Prevent redirects
|
||||
.build();
|
||||
authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
|
||||
}
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
authorizationCodeRequestAuthentication.getAuthorizationUri(), authorizationCodeRequestAuthentication.getClientId(),
|
||||
(Authentication) authorizationCodeRequestAuthentication.getPrincipal(), redirectUri,
|
||||
authorizationCodeRequestAuthentication.getState(), authorizationCodeRequestAuthentication.getScopes(),
|
||||
authorizationCodeRequestAuthentication.getAdditionalParameters());
|
||||
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
|
||||
}
|
||||
@@ -541,152 +360,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
return null;
|
||||
}
|
||||
|
||||
private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(authorizationCodeRequestAuthentication.getClientId(), (Authentication) authorizationCodeRequestAuthentication.getPrincipal())
|
||||
.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
|
||||
.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
|
||||
.scopes(authorizationCodeRequestAuthentication.getScopes())
|
||||
.state(authorizationCodeRequestAuthentication.getState())
|
||||
.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
|
||||
.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
|
||||
}
|
||||
|
||||
private static class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator<OAuth2AuthorizationCode> {
|
||||
private final StringKeyGenerator authorizationCodeGenerator =
|
||||
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public OAuth2AuthorizationCode generate(OAuth2TokenContext context) {
|
||||
if (context.getTokenType() == null ||
|
||||
!OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) {
|
||||
return null;
|
||||
}
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getAuthorizationCodeTimeToLive());
|
||||
return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class DefaultRedirectUriOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
|
||||
|
||||
@Override
|
||||
public void validate(OAuth2AuthenticationContext authenticationContext) {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
authenticationContext.getAuthentication();
|
||||
RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class);
|
||||
|
||||
String requestedRedirectUri = authorizationCodeRequestAuthentication.getRedirectUri();
|
||||
|
||||
if (StringUtils.hasText(requestedRedirectUri)) {
|
||||
// ***** redirect_uri is available in authorization request
|
||||
|
||||
UriComponents requestedRedirect = null;
|
||||
try {
|
||||
requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build();
|
||||
} catch (Exception ex) { }
|
||||
if (requestedRedirect == null || requestedRedirect.getFragment() != null) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
|
||||
String requestedRedirectHost = requestedRedirect.getHost();
|
||||
if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) {
|
||||
// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1
|
||||
// While redirect URIs using localhost (i.e., "http://localhost:{port}/{path}")
|
||||
// function similarly to loopback IP redirects described in Section 10.3.3,
|
||||
// the use of "localhost" is NOT RECOMMENDED.
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
"localhost is not allowed for the redirect_uri (" + requestedRedirectUri + "). " +
|
||||
"Use the IP literal (127.0.0.1) instead.",
|
||||
"https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1");
|
||||
throwError(error, OAuth2ParameterNames.REDIRECT_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient, null);
|
||||
}
|
||||
|
||||
if (!isLoopbackAddress(requestedRedirectHost)) {
|
||||
// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7
|
||||
// When comparing client redirect URIs against pre-registered URIs,
|
||||
// authorization servers MUST utilize exact string matching.
|
||||
if (!registeredClient.getRedirectUris().contains(requestedRedirectUri)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
} else {
|
||||
// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-10.3.3
|
||||
// The authorization server MUST allow any port to be specified at the
|
||||
// time of the request for loopback IP redirect URIs, to accommodate
|
||||
// clients that obtain an available ephemeral port from the operating
|
||||
// system at the time of the request.
|
||||
boolean validRedirectUri = false;
|
||||
for (String registeredRedirectUri : registeredClient.getRedirectUris()) {
|
||||
UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri);
|
||||
registeredRedirect.port(requestedRedirect.getPort());
|
||||
if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) {
|
||||
validRedirectUri = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!validRedirectUri) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// ***** redirect_uri is NOT available in authorization request
|
||||
|
||||
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
|
||||
registeredClient.getRedirectUris().size() != 1) {
|
||||
// redirect_uri is REQUIRED for OpenID Connect
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLoopbackAddress(String host) {
|
||||
// IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1"
|
||||
if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) {
|
||||
return true;
|
||||
}
|
||||
// IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255
|
||||
String[] ipv4Octets = host.split("\\.");
|
||||
if (ipv4Octets.length != 4) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
int[] address = new int[ipv4Octets.length];
|
||||
for (int i=0; i < ipv4Octets.length; i++) {
|
||||
address[i] = Integer.parseInt(ipv4Octets[i]);
|
||||
}
|
||||
return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 &&
|
||||
address[2] <= 255 && address[3] >= 1 && address[3] <= 255;
|
||||
} catch (NumberFormatException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class DefaultScopeOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
|
||||
|
||||
@Override
|
||||
public void validate(OAuth2AuthenticationContext authenticationContext) {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
authenticationContext.getAuthentication();
|
||||
RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class);
|
||||
|
||||
Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
|
||||
Set<String> allowedScopes = registeredClient.getScopes();
|
||||
if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,45 +15,104 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation for the OAuth 2.0 Authorization Request (and Consent)
|
||||
* An {@link Authentication} implementation for the OAuth 2.0 Authorization Request
|
||||
* used in the Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.2
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider
|
||||
*/
|
||||
public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
|
||||
public class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
|
||||
private String authorizationUri;
|
||||
private String clientId;
|
||||
private Authentication principal;
|
||||
private String redirectUri;
|
||||
private Set<String> scopes;
|
||||
private String state;
|
||||
private Map<String, Object> additionalParameters;
|
||||
private boolean consentRequired;
|
||||
private boolean consent;
|
||||
private OAuth2AuthorizationCode authorizationCode;
|
||||
private final String authorizationUri;
|
||||
private final String clientId;
|
||||
private final Authentication principal;
|
||||
private final String redirectUri;
|
||||
private final String state;
|
||||
private final Set<String> scopes;
|
||||
private final Map<String, Object> additionalParameters;
|
||||
private final OAuth2AuthorizationCode authorizationCode;
|
||||
|
||||
private OAuth2AuthorizationCodeRequestAuthenticationToken() {
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param authorizationUri the authorization URI
|
||||
* @param clientId the client identifier
|
||||
* @param principal the {@code Principal} (Resource Owner)
|
||||
* @param redirectUri the redirect uri
|
||||
* @param state the state
|
||||
* @param scopes the requested scope(s)
|
||||
* @param additionalParameters the additional parameters
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId, Authentication principal,
|
||||
@Nullable String redirectUri, @Nullable String state, @Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
|
||||
super(Collections.emptyList());
|
||||
Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
this.authorizationUri = authorizationUri;
|
||||
this.clientId = clientId;
|
||||
this.principal = principal;
|
||||
this.redirectUri = redirectUri;
|
||||
this.state = state;
|
||||
this.scopes = Collections.unmodifiableSet(
|
||||
scopes != null ?
|
||||
new HashSet<>(scopes) :
|
||||
Collections.emptySet());
|
||||
this.additionalParameters = Collections.unmodifiableMap(
|
||||
additionalParameters != null ?
|
||||
new HashMap<>(additionalParameters) :
|
||||
Collections.emptyMap());
|
||||
this.authorizationCode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param authorizationUri the authorization URI
|
||||
* @param clientId the client identifier
|
||||
* @param principal the {@code Principal} (Resource Owner)
|
||||
* @param authorizationCode the {@link OAuth2AuthorizationCode}
|
||||
* @param redirectUri the redirect uri
|
||||
* @param state the state
|
||||
* @param scopes the authorized scope(s)
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId, Authentication principal,
|
||||
OAuth2AuthorizationCode authorizationCode, @Nullable String redirectUri, @Nullable String state, @Nullable Set<String> scopes) {
|
||||
super(Collections.emptyList());
|
||||
Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
Assert.notNull(authorizationCode, "authorizationCode cannot be null");
|
||||
this.authorizationUri = authorizationUri;
|
||||
this.clientId = clientId;
|
||||
this.principal = principal;
|
||||
this.authorizationCode = authorizationCode;
|
||||
this.redirectUri = redirectUri;
|
||||
this.state = state;
|
||||
this.scopes = Collections.unmodifiableSet(
|
||||
scopes != null ?
|
||||
new HashSet<>(scopes) :
|
||||
Collections.emptySet());
|
||||
this.additionalParameters = Collections.emptyMap();
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,15 +153,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs
|
||||
return this.redirectUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested (or authorized) scope(s).
|
||||
*
|
||||
* @return the requested (or authorized) scope(s), or an empty {@code Set} if not available
|
||||
*/
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state.
|
||||
*
|
||||
@@ -113,34 +163,24 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested (or authorized) scope(s).
|
||||
*
|
||||
* @return the requested (or authorized) scope(s), or an empty {@code Set} if not available
|
||||
*/
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional parameters.
|
||||
*
|
||||
* @return the additional parameters
|
||||
* @return the additional parameters, or an empty {@code Map} if not available
|
||||
*/
|
||||
public Map<String, Object> getAdditionalParameters() {
|
||||
return this.additionalParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if authorization consent is required, {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if authorization consent is required, {@code false} otherwise
|
||||
*/
|
||||
public boolean isConsentRequired() {
|
||||
return this.consentRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this {@code Authentication} represents an authorization consent request,
|
||||
* {@code false} otherwise.
|
||||
*
|
||||
* @return {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
|
||||
*/
|
||||
public boolean isConsent() {
|
||||
return this.consent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
@@ -151,170 +191,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs
|
||||
return this.authorizationCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Builder}, initialized with the given client identifier
|
||||
* and {@code Principal} (Resource Owner).
|
||||
*
|
||||
* @param clientId the client identifier
|
||||
* @param principal the {@code Principal} (Resource Owner)
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(@NonNull String clientId, @NonNull Authentication principal) {
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
return new Builder(clientId, principal);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
*/
|
||||
public static final class Builder implements Serializable {
|
||||
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
|
||||
private String authorizationUri;
|
||||
private String clientId;
|
||||
private Authentication principal;
|
||||
private String redirectUri;
|
||||
private Set<String> scopes;
|
||||
private String state;
|
||||
private Map<String, Object> additionalParameters;
|
||||
private boolean consentRequired;
|
||||
private boolean consent;
|
||||
private OAuth2AuthorizationCode authorizationCode;
|
||||
|
||||
private Builder(String clientId, Authentication principal) {
|
||||
this.clientId = clientId;
|
||||
this.principal = principal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authorization URI.
|
||||
*
|
||||
* @param authorizationUri the authorization URI
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder authorizationUri(String authorizationUri) {
|
||||
this.authorizationUri = authorizationUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the redirect uri.
|
||||
*
|
||||
* @param redirectUri the redirect uri
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder redirectUri(String redirectUri) {
|
||||
this.redirectUri = redirectUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the requested (or authorized) scope(s).
|
||||
*
|
||||
* @param scopes the requested (or authorized) scope(s)
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder scopes(Set<String> scopes) {
|
||||
if (scopes != null) {
|
||||
this.scopes = new HashSet<>(scopes);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state.
|
||||
*
|
||||
* @param state the state
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder state(String state) {
|
||||
this.state = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the additional parameters.
|
||||
*
|
||||
* @param additionalParameters the additional parameters
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder additionalParameters(Map<String, Object> additionalParameters) {
|
||||
if (additionalParameters != null) {
|
||||
this.additionalParameters = new HashMap<>(additionalParameters);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} if authorization consent is required, {@code false} otherwise.
|
||||
*
|
||||
* @param consentRequired {@code true} if authorization consent is required, {@code false} otherwise
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder consentRequired(boolean consentRequired) {
|
||||
this.consentRequired = consentRequired;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise.
|
||||
*
|
||||
* @param consent {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder consent(boolean consent) {
|
||||
this.consent = consent;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @param authorizationCode the {@link OAuth2AuthorizationCode}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public Builder authorizationCode(OAuth2AuthorizationCode authorizationCode) {
|
||||
this.authorizationCode = authorizationCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
*
|
||||
* @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
*/
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationToken build() {
|
||||
Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty");
|
||||
if (this.consent) {
|
||||
Assert.hasText(this.state, "state cannot be empty");
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationToken();
|
||||
|
||||
authentication.authorizationUri = this.authorizationUri;
|
||||
authentication.clientId = this.clientId;
|
||||
authentication.principal = this.principal;
|
||||
authentication.redirectUri = this.redirectUri;
|
||||
authentication.scopes = Collections.unmodifiableSet(
|
||||
!CollectionUtils.isEmpty(this.scopes) ?
|
||||
this.scopes :
|
||||
Collections.emptySet());
|
||||
authentication.state = this.state;
|
||||
authentication.additionalParameters = Collections.unmodifiableMap(
|
||||
!CollectionUtils.isEmpty(this.additionalParameters) ?
|
||||
this.additionalParameters :
|
||||
Collections.emptyMap());
|
||||
authentication.consentRequired = this.consentRequired;
|
||||
authentication.consent = this.consent;
|
||||
authentication.authorizationCode = this.authorizationCode;
|
||||
if (this.authorizationCode != null || this.consentRequired) {
|
||||
authentication.setAuthenticated(true);
|
||||
}
|
||||
|
||||
return authentication;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
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.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
|
||||
* containing an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
* and is the default {@link OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer) authentication validator}
|
||||
* used for validating specific OAuth 2.0 Authorization Request parameters used in the Authorization Code Grant.
|
||||
*
|
||||
* <p>
|
||||
* The default implementation first validates {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()}
|
||||
* and then {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}.
|
||||
* If validation fails, an {@link OAuth2AuthorizationCodeRequestAuthenticationException} is thrown.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationContext
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
*/
|
||||
public final class OAuth2AuthorizationCodeRequestAuthenticationValidator implements Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
|
||||
|
||||
/**
|
||||
* The default validator for {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}.
|
||||
*/
|
||||
public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_SCOPE_VALIDATOR =
|
||||
OAuth2AuthorizationCodeRequestAuthenticationValidator::validateScope;
|
||||
|
||||
/**
|
||||
* The default validator for {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()}.
|
||||
*/
|
||||
public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR =
|
||||
OAuth2AuthorizationCodeRequestAuthenticationValidator::validateRedirectUri;
|
||||
|
||||
private final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator =
|
||||
DEFAULT_REDIRECT_URI_VALIDATOR.andThen(DEFAULT_SCOPE_VALIDATOR);
|
||||
|
||||
@Override
|
||||
public void accept(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
|
||||
this.authenticationValidator.accept(authenticationContext);
|
||||
}
|
||||
|
||||
private static void validateScope(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
authenticationContext.getAuthentication();
|
||||
RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
|
||||
|
||||
Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
|
||||
Set<String> allowedScopes = registeredClient.getScopes();
|
||||
if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateRedirectUri(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
authenticationContext.getAuthentication();
|
||||
RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
|
||||
|
||||
String requestedRedirectUri = authorizationCodeRequestAuthentication.getRedirectUri();
|
||||
|
||||
if (StringUtils.hasText(requestedRedirectUri)) {
|
||||
// ***** redirect_uri is available in authorization request
|
||||
|
||||
UriComponents requestedRedirect = null;
|
||||
try {
|
||||
requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build();
|
||||
} catch (Exception ex) { }
|
||||
if (requestedRedirect == null || requestedRedirect.getFragment() != null) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
|
||||
String requestedRedirectHost = requestedRedirect.getHost();
|
||||
if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) {
|
||||
// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-9.7.1
|
||||
// While redirect URIs using localhost (i.e., "http://localhost:{port}/{path}")
|
||||
// function similarly to loopback IP redirects described in Section 10.3.3,
|
||||
// the use of "localhost" is NOT RECOMMENDED.
|
||||
OAuth2Error error = new OAuth2Error(
|
||||
OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
"localhost is not allowed for the redirect_uri (" + requestedRedirectUri + "). " +
|
||||
"Use the IP literal (127.0.0.1) instead.",
|
||||
"https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-9.7.1");
|
||||
throwError(error, OAuth2ParameterNames.REDIRECT_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
|
||||
if (!isLoopbackAddress(requestedRedirectHost)) {
|
||||
// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-9.7
|
||||
// When comparing client redirect URIs against pre-registered URIs,
|
||||
// authorization servers MUST utilize exact string matching.
|
||||
if (!registeredClient.getRedirectUris().contains(requestedRedirectUri)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
} else {
|
||||
// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-10.3.3
|
||||
// The authorization server MUST allow any port to be specified at the
|
||||
// time of the request for loopback IP redirect URIs, to accommodate
|
||||
// clients that obtain an available ephemeral port from the operating
|
||||
// system at the time of the request.
|
||||
boolean validRedirectUri = false;
|
||||
for (String registeredRedirectUri : registeredClient.getRedirectUris()) {
|
||||
UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri);
|
||||
registeredRedirect.port(requestedRedirect.getPort());
|
||||
if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) {
|
||||
validRedirectUri = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!validRedirectUri) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// ***** redirect_uri is NOT available in authorization request
|
||||
|
||||
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
|
||||
registeredClient.getRedirectUris().size() != 1) {
|
||||
// redirect_uri is REQUIRED for OpenID Connect
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLoopbackAddress(String host) {
|
||||
// IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1"
|
||||
if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) {
|
||||
return true;
|
||||
}
|
||||
// IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255
|
||||
String[] ipv4Octets = host.split("\\.");
|
||||
if (ipv4Octets.length != 4) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
int[] address = new int[ipv4Octets.length];
|
||||
for (int i=0; i < ipv4Octets.length; i++) {
|
||||
address[i] = Integer.parseInt(ipv4Octets[i]);
|
||||
}
|
||||
return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 &&
|
||||
address[2] <= 255 && address[3] >= 1 && address[3] <= 255;
|
||||
} catch (NumberFormatException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
|
||||
throwError(error, parameterName, authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
|
||||
private static void throwError(OAuth2Error error, String parameterName,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient) {
|
||||
|
||||
String redirectUri = StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri()) ?
|
||||
authorizationCodeRequestAuthentication.getRedirectUri() :
|
||||
registeredClient.getRedirectUris().iterator().next();
|
||||
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
|
||||
parameterName.equals(OAuth2ParameterNames.REDIRECT_URI)) {
|
||||
redirectUri = null; // Prevent redirects
|
||||
}
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
authorizationCodeRequestAuthentication.getAuthorizationUri(), authorizationCodeRequestAuthentication.getClientId(),
|
||||
(Authentication) authorizationCodeRequestAuthentication.getPrincipal(), redirectUri,
|
||||
authorizationCodeRequestAuthentication.getState(), authorizationCodeRequestAuthentication.getScopes(),
|
||||
authorizationCodeRequestAuthentication.getAdditionalParameters());
|
||||
authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
|
||||
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,8 +15,12 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||
@@ -32,11 +36,26 @@ import org.springframework.util.Assert;
|
||||
* @since 0.2.1
|
||||
* @see OAuth2AuthenticationContext
|
||||
* @see OAuth2AuthorizationConsent
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider#setAuthorizationConsentCustomizer(Consumer)
|
||||
*/
|
||||
public final class OAuth2AuthorizationConsentAuthenticationContext extends OAuth2AuthenticationContext {
|
||||
public final class OAuth2AuthorizationConsentAuthenticationContext implements OAuth2AuthenticationContext {
|
||||
private final Map<Object, Object> context;
|
||||
|
||||
private OAuth2AuthorizationConsentAuthenticationContext(Map<Object, Object> context) {
|
||||
super(context);
|
||||
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
return this.context.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,12 +95,12 @@ public final class OAuth2AuthorizationConsentAuthenticationContext extends OAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
|
||||
* Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationConsentAuthenticationToken}.
|
||||
*
|
||||
* @param authentication the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
* @param authentication the {@link OAuth2AuthorizationConsentAuthenticationToken}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
|
||||
public static Builder with(OAuth2AuthorizationConsentAuthenticationToken authentication) {
|
||||
return new Builder(authentication);
|
||||
}
|
||||
|
||||
@@ -90,7 +109,7 @@ public final class OAuth2AuthorizationConsentAuthenticationContext extends OAuth
|
||||
*/
|
||||
public static final class Builder extends AbstractBuilder<OAuth2AuthorizationConsentAuthenticationContext, Builder> {
|
||||
|
||||
private Builder(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
|
||||
private Builder(OAuth2AuthorizationConsentAuthenticationToken authentication) {
|
||||
super(authentication);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* 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.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
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.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Consent
|
||||
* used in the Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see OAuth2AuthorizationConsentAuthenticationToken
|
||||
* @see OAuth2AuthorizationConsent
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
* @see RegisteredClientRepository
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OAuth2AuthorizationConsentService
|
||||
*/
|
||||
public final class OAuth2AuthorizationConsentAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
|
||||
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2AuthorizationConsentService authorizationConsentService;
|
||||
private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
|
||||
private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationConsentAuthenticationProvider} using the provided parameters.
|
||||
*
|
||||
* @param registeredClientRepository the repository of registered clients
|
||||
* @param authorizationService the authorization service
|
||||
* @param authorizationConsentService the authorization consent service
|
||||
*/
|
||||
public OAuth2AuthorizationConsentAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
|
||||
OAuth2AuthorizationService authorizationService, OAuth2AuthorizationConsentService authorizationConsentService) {
|
||||
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
|
||||
this.registeredClientRepository = registeredClientRepository;
|
||||
this.authorizationService = authorizationService;
|
||||
this.authorizationConsentService = authorizationConsentService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication =
|
||||
(OAuth2AuthorizationConsentAuthenticationToken) authentication;
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
authorizationConsentAuthentication.getState(), STATE_TOKEN_TYPE);
|
||||
if (authorization == null) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
|
||||
authorizationConsentAuthentication, null, null);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved authorization with authorization consent state");
|
||||
}
|
||||
|
||||
// The 'in-flight' authorization must be associated to the current principal
|
||||
Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal();
|
||||
if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
|
||||
authorizationConsentAuthentication, null, null);
|
||||
}
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
authorizationConsentAuthentication.getClientId());
|
||||
if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationConsentAuthentication, registeredClient, null);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved registered client");
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
Set<String> requestedScopes = authorizationRequest.getScopes();
|
||||
Set<String> authorizedScopes = new HashSet<>(authorizationConsentAuthentication.getScopes());
|
||||
if (!requestedScopes.containsAll(authorizedScopes)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
|
||||
authorizationConsentAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated authorization consent request parameters");
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName());
|
||||
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
|
||||
currentAuthorizationConsent.getScopes() : Collections.emptySet();
|
||||
|
||||
if (!currentAuthorizedScopes.isEmpty()) {
|
||||
for (String requestedScope : requestedScopes) {
|
||||
if (currentAuthorizedScopes.contains(requestedScope)) {
|
||||
authorizedScopes.add(requestedScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!authorizedScopes.isEmpty() && requestedScopes.contains(OidcScopes.OPENID)) {
|
||||
// 'openid' scope is auto-approved as it does not require consent
|
||||
authorizedScopes.add(OidcScopes.OPENID);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
|
||||
if (currentAuthorizationConsent != null) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved existing authorization consent");
|
||||
}
|
||||
authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
|
||||
} else {
|
||||
authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
|
||||
authorization.getRegisteredClientId(), authorization.getPrincipalName());
|
||||
}
|
||||
authorizedScopes.forEach(authorizationConsentBuilder::scope);
|
||||
|
||||
if (this.authorizationConsentCustomizer != null) {
|
||||
// @formatter:off
|
||||
OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =
|
||||
OAuth2AuthorizationConsentAuthenticationContext.with(authorizationConsentAuthentication)
|
||||
.authorizationConsent(authorizationConsentBuilder)
|
||||
.registeredClient(registeredClient)
|
||||
.authorization(authorization)
|
||||
.authorizationRequest(authorizationRequest)
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Customized authorization consent");
|
||||
}
|
||||
}
|
||||
|
||||
Set<GrantedAuthority> authorities = new HashSet<>();
|
||||
authorizationConsentBuilder.authorities(authorities::addAll);
|
||||
|
||||
if (authorities.isEmpty()) {
|
||||
// Authorization consent denied (or revoked)
|
||||
if (currentAuthorizationConsent != null) {
|
||||
this.authorizationConsentService.remove(currentAuthorizationConsent);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Revoked authorization consent");
|
||||
}
|
||||
}
|
||||
this.authorizationService.remove(authorization);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Removed authorization");
|
||||
}
|
||||
throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationConsentAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
|
||||
if (!authorizationConsent.equals(currentAuthorizationConsent)) {
|
||||
this.authorizationConsentService.save(authorizationConsent);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Saved authorization consent");
|
||||
}
|
||||
}
|
||||
|
||||
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
|
||||
authorizationConsentAuthentication, registeredClient, authorization, authorizedScopes);
|
||||
OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
|
||||
if (authorizationCode == null) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the authorization code.", ERROR_URI);
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Generated authorization code");
|
||||
}
|
||||
|
||||
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.token(authorizationCode)
|
||||
.attributes(attrs -> {
|
||||
attrs.remove(OAuth2ParameterNames.STATE);
|
||||
})
|
||||
.build();
|
||||
this.authorizationService.save(updatedAuthorization);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Saved authorization");
|
||||
}
|
||||
|
||||
String redirectUri = authorizationRequest.getRedirectUri();
|
||||
if (!StringUtils.hasText(redirectUri)) {
|
||||
redirectUri = registeredClient.getRedirectUris().iterator().next();
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated authorization consent request");
|
||||
}
|
||||
|
||||
return new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
authorizationRequest.getAuthorizationUri(), registeredClient.getClientId(), principal, authorizationCode,
|
||||
redirectUri, authorizationRequest.getState(), authorizedScopes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return OAuth2AuthorizationConsentAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
|
||||
*
|
||||
* @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
|
||||
*/
|
||||
public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
|
||||
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
|
||||
this.authorizationCodeGenerator = authorizationCodeGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationConsentAuthenticationContext}
|
||||
* containing an {@link OAuth2AuthorizationConsent.Builder} and additional context information.
|
||||
*
|
||||
* <p>
|
||||
* The following context attributes are available:
|
||||
* <ul>
|
||||
* <li>The {@link OAuth2AuthorizationConsent.Builder} used to build the authorization consent
|
||||
* prior to {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}.</li>
|
||||
* <li>The {@link Authentication} of type
|
||||
* {@link OAuth2AuthorizationConsentAuthenticationToken}.</li>
|
||||
* <li>The {@link RegisteredClient} associated with the authorization request.</li>
|
||||
* <li>The {@link OAuth2Authorization} associated with the state token presented in the
|
||||
* authorization consent request.</li>
|
||||
* <li>The {@link OAuth2AuthorizationRequest} associated with the authorization consent request.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param authorizationConsentCustomizer the {@code Consumer} providing access to the
|
||||
* {@link OAuth2AuthorizationConsentAuthenticationContext} containing an {@link OAuth2AuthorizationConsent.Builder}
|
||||
*/
|
||||
public void setAuthorizationConsentCustomizer(Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer) {
|
||||
Assert.notNull(authorizationConsentCustomizer, "authorizationConsentCustomizer cannot be null");
|
||||
this.authorizationConsentCustomizer = authorizationConsentCustomizer;
|
||||
}
|
||||
|
||||
private static OAuth2TokenContext createAuthorizationCodeTokenContext(
|
||||
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2Authorization authorization, Set<String> authorizedScopes) {
|
||||
|
||||
// @formatter:off
|
||||
return DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal((Authentication) authorizationConsentAuthentication.getPrincipal())
|
||||
.authorization(authorization)
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.tokenType(new OAuth2TokenType(OAuth2ParameterNames.CODE))
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrant(authorizationConsentAuthentication)
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static boolean isPrincipalAuthenticated(Authentication principal) {
|
||||
return principal != null &&
|
||||
!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
|
||||
principal.isAuthenticated();
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName,
|
||||
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
|
||||
throwError(error, parameterName, authorizationConsentAuthentication, registeredClient, authorizationRequest);
|
||||
}
|
||||
|
||||
private static void throwError(OAuth2Error error, String parameterName,
|
||||
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication,
|
||||
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
|
||||
|
||||
String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
|
||||
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
|
||||
(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
|
||||
parameterName.equals(OAuth2ParameterNames.STATE))) {
|
||||
redirectUri = null; // Prevent redirects
|
||||
}
|
||||
|
||||
String state = authorizationRequest != null ?
|
||||
authorizationRequest.getState() :
|
||||
authorizationConsentAuthentication.getState();
|
||||
Set<String> requestedScopes = authorizationRequest != null ?
|
||||
authorizationRequest.getScopes() :
|
||||
authorizationConsentAuthentication.getScopes();
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
authorizationConsentAuthentication.getAuthorizationUri(), authorizationConsentAuthentication.getClientId(),
|
||||
(Authentication) authorizationConsentAuthentication.getPrincipal(), redirectUri,
|
||||
state, requestedScopes, null);
|
||||
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
|
||||
}
|
||||
|
||||
private static String resolveRedirectUri(OAuth2AuthorizationRequest authorizationRequest, RegisteredClient registeredClient) {
|
||||
if (authorizationRequest != null && StringUtils.hasText(authorizationRequest.getRedirectUri())) {
|
||||
return authorizationRequest.getRedirectUri();
|
||||
}
|
||||
if (registeredClient != null) {
|
||||
return registeredClient.getRedirectUris().iterator().next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation for the OAuth 2.0 Authorization Consent
|
||||
* used in the Authorization Code Grant.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
*/
|
||||
public class OAuth2AuthorizationConsentAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
|
||||
private final String authorizationUri;
|
||||
private final String clientId;
|
||||
private final Authentication principal;
|
||||
private final String state;
|
||||
private final Set<String> scopes;
|
||||
private final Map<String, Object> additionalParameters;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationConsentAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param authorizationUri the authorization URI
|
||||
* @param clientId the client identifier
|
||||
* @param principal the {@code Principal} (Resource Owner)
|
||||
* @param state the state
|
||||
* @param scopes the requested (or authorized) scope(s)
|
||||
* @param additionalParameters the additional parameters
|
||||
*/
|
||||
public OAuth2AuthorizationConsentAuthenticationToken(String authorizationUri, String clientId, Authentication principal,
|
||||
String state, @Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
|
||||
super(Collections.emptyList());
|
||||
Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
Assert.hasText(state, "state cannot be empty");
|
||||
this.authorizationUri = authorizationUri;
|
||||
this.clientId = clientId;
|
||||
this.principal = principal;
|
||||
this.state = state;
|
||||
this.scopes = Collections.unmodifiableSet(
|
||||
scopes != null ?
|
||||
new HashSet<>(scopes) :
|
||||
Collections.emptySet());
|
||||
this.additionalParameters = Collections.unmodifiableMap(
|
||||
additionalParameters != null ?
|
||||
new HashMap<>(additionalParameters) :
|
||||
Collections.emptyMap());
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorization URI.
|
||||
*
|
||||
* @return the authorization URI
|
||||
*/
|
||||
public String getAuthorizationUri() {
|
||||
return this.authorizationUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the client identifier.
|
||||
*
|
||||
* @return the client identifier
|
||||
*/
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state.
|
||||
*
|
||||
* @return the state
|
||||
*/
|
||||
public String getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested (or authorized) scope(s).
|
||||
*
|
||||
* @return the requested (or authorized) scope(s), or an empty {@code Set} if not available
|
||||
*/
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional parameters.
|
||||
*
|
||||
* @return the additional parameters, or an empty {@code Map} if not available
|
||||
*/
|
||||
public Map<String, Object> getAdditionalParameters() {
|
||||
return this.additionalParameters;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,6 +19,9 @@ import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
@@ -33,7 +36,7 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
|
||||
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.context.ProviderContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
@@ -57,6 +60,7 @@ import static org.springframework.security.oauth2.server.authorization.authentic
|
||||
*/
|
||||
public final class OAuth2ClientCredentialsAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
|
||||
@@ -84,6 +88,10 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
|
||||
getAuthenticatedClientElseThrowInvalidClient(clientCredentialsAuthentication);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved registered client");
|
||||
}
|
||||
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.CLIENT_CREDENTIALS)) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
|
||||
}
|
||||
@@ -98,11 +106,15 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
|
||||
authorizedScopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated token request parameters");
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(clientPrincipal)
|
||||
.providerContext(ProviderContextHolder.getProviderContext())
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
@@ -116,6 +128,11 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
|
||||
"The token generator failed to generate the access token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Generated access token");
|
||||
}
|
||||
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
@@ -137,6 +154,12 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
|
||||
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Saved authorization");
|
||||
// This log is kept separate for consistency with other providers
|
||||
this.logger.trace("Authenticated token request");
|
||||
}
|
||||
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
@@ -40,7 +43,7 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
|
||||
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.context.ProviderContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
@@ -65,6 +68,7 @@ import static org.springframework.security.oauth2.server.authorization.authentic
|
||||
public final class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
|
||||
@@ -92,12 +96,20 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
|
||||
getAuthenticatedClientElseThrowInvalidClient(refreshTokenAuthentication);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved registered client");
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
refreshTokenAuthentication.getRefreshToken(), OAuth2TokenType.REFRESH_TOKEN);
|
||||
if (authorization == null) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved authorization with refresh token");
|
||||
}
|
||||
|
||||
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
@@ -122,6 +134,11 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
|
||||
if (!authorizedScopes.containsAll(scopes)) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated token request parameters");
|
||||
}
|
||||
|
||||
if (scopes.isEmpty()) {
|
||||
scopes = authorizedScopes;
|
||||
}
|
||||
@@ -130,7 +147,7 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(authorization.getAttribute(Principal.class.getName()))
|
||||
.providerContext(ProviderContextHolder.getProviderContext())
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.authorization(authorization)
|
||||
.authorizedScopes(scopes)
|
||||
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
|
||||
@@ -147,6 +164,11 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
|
||||
"The token generator failed to generate the access token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Generated access token");
|
||||
}
|
||||
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
@@ -169,6 +191,11 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
|
||||
"The token generator failed to generate the refresh token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Generated refresh token");
|
||||
}
|
||||
|
||||
currentRefreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||
authorizationBuilder.refreshToken(currentRefreshToken);
|
||||
}
|
||||
@@ -188,6 +215,11 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
|
||||
"The token generator failed to generate the ID token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Generated id token");
|
||||
}
|
||||
|
||||
idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
|
||||
generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
|
||||
authorizationBuilder.token(idToken, (metadata) ->
|
||||
@@ -200,12 +232,20 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
|
||||
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Saved authorization");
|
||||
}
|
||||
|
||||
Map<String, Object> additionalParameters = Collections.emptyMap();
|
||||
if (idToken != null) {
|
||||
additionalParameters = new HashMap<>();
|
||||
additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated token request");
|
||||
}
|
||||
|
||||
return new OAuth2AccessTokenAuthenticationToken(
|
||||
registeredClient, clientPrincipal, accessToken, currentRefreshToken, additionalParameters);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -53,6 +56,7 @@ public final class OAuth2TokenIntrospectionAuthenticationProvider implements Aut
|
||||
private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
|
||||
private static final TypeDescriptor LIST_STRING_TYPE_DESCRIPTOR =
|
||||
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class));
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
@@ -81,13 +85,23 @@ public final class OAuth2TokenIntrospectionAuthenticationProvider implements Aut
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
tokenIntrospectionAuthentication.getToken(), null);
|
||||
if (authorization == null) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Did not authenticate token introspection request since token was not found");
|
||||
}
|
||||
// Return the authentication request when token not found
|
||||
return tokenIntrospectionAuthentication;
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved authorization with token");
|
||||
}
|
||||
|
||||
OAuth2Authorization.Token<OAuth2Token> authorizedToken =
|
||||
authorization.getToken(tokenIntrospectionAuthentication.getToken());
|
||||
if (!authorizedToken.isActive()) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Did not introspect token since not active");
|
||||
}
|
||||
return new OAuth2TokenIntrospectionAuthenticationToken(tokenIntrospectionAuthentication.getToken(),
|
||||
clientPrincipal, OAuth2TokenIntrospection.builder().build());
|
||||
}
|
||||
@@ -95,6 +109,10 @@ public final class OAuth2TokenIntrospectionAuthenticationProvider implements Aut
|
||||
RegisteredClient authorizedClient = this.registeredClientRepository.findById(authorization.getRegisteredClientId());
|
||||
OAuth2TokenIntrospection tokenClaims = withActiveTokenClaims(authorizedToken, authorizedClient);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated token introspection request");
|
||||
}
|
||||
|
||||
return new OAuth2TokenIntrospectionAuthenticationToken(authorizedToken.getToken().getTokenValue(),
|
||||
clientPrincipal, tokenClaims);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
@@ -39,6 +42,7 @@ import static org.springframework.security.oauth2.server.authorization.authentic
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7009#section-2.1">Section 2.1 Revocation Request</a>
|
||||
*/
|
||||
public final class OAuth2TokenRevocationAuthenticationProvider implements AuthenticationProvider {
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
/**
|
||||
@@ -63,6 +67,9 @@ public final class OAuth2TokenRevocationAuthenticationProvider implements Authen
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
tokenRevocationAuthentication.getToken(), null);
|
||||
if (authorization == null) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Did not authenticate token revocation request since token was not found");
|
||||
}
|
||||
// Return the authentication request when token not found
|
||||
return tokenRevocationAuthentication;
|
||||
}
|
||||
@@ -75,6 +82,12 @@ public final class OAuth2TokenRevocationAuthenticationProvider implements Authen
|
||||
authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, token.getToken());
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Saved authorization with revoked token");
|
||||
// This log is kept separate for consistency with other providers
|
||||
this.logger.trace("Authenticated token revocation request");
|
||||
}
|
||||
|
||||
return new OAuth2TokenRevocationAuthenticationToken(token.getToken(), clientPrincipal);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
@@ -42,6 +45,7 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public final class PublicClientAuthenticationProvider implements AuthenticationProvider {
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final RegisteredClientRepository registeredClientRepository;
|
||||
private final CodeVerifierAuthenticator codeVerifierAuthenticator;
|
||||
|
||||
@@ -74,14 +78,26 @@ public final class PublicClientAuthenticationProvider implements AuthenticationP
|
||||
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved registered client");
|
||||
}
|
||||
|
||||
if (!registeredClient.getClientAuthenticationMethods().contains(
|
||||
clientAuthentication.getClientAuthenticationMethod())) {
|
||||
throwInvalidClient("authentication_method");
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated client authentication parameters");
|
||||
}
|
||||
|
||||
// Validate the "code_verifier" parameter for the public client
|
||||
this.codeVerifierAuthenticator.authenticateRequired(clientAuthentication, registeredClient);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated public client");
|
||||
}
|
||||
|
||||
return new OAuth2ClientAuthenticationToken(registeredClient,
|
||||
clientAuthentication.getClientAuthenticationMethod(), null);
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
|
||||
+ " WHERE " + PK_FILTER;
|
||||
// @formatter:on
|
||||
|
||||
private static final String COUNT_REGISTERED_CLIENT_SQL = "SELECT COUNT(*) FROM " + TABLE_NAME + " WHERE ";
|
||||
|
||||
private final JdbcOperations jdbcOperations;
|
||||
private RowMapper<RegisteredClient> registeredClientRowMapper;
|
||||
private Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper;
|
||||
@@ -141,11 +143,31 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
|
||||
}
|
||||
|
||||
private void insertRegisteredClient(RegisteredClient registeredClient) {
|
||||
assertUniqueIdentifiers(registeredClient);
|
||||
List<SqlParameterValue> parameters = this.registeredClientParametersMapper.apply(registeredClient);
|
||||
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
|
||||
this.jdbcOperations.update(INSERT_REGISTERED_CLIENT_SQL, pss);
|
||||
}
|
||||
|
||||
private void assertUniqueIdentifiers(RegisteredClient registeredClient) {
|
||||
Integer count = this.jdbcOperations.queryForObject(
|
||||
COUNT_REGISTERED_CLIENT_SQL + "client_id = ?",
|
||||
Integer.class,
|
||||
registeredClient.getClientId());
|
||||
if (count != null && count > 0) {
|
||||
throw new IllegalArgumentException("Registered client must be unique. " +
|
||||
"Found duplicate client identifier: " + registeredClient.getClientId());
|
||||
}
|
||||
count = this.jdbcOperations.queryForObject(
|
||||
COUNT_REGISTERED_CLIENT_SQL + "client_secret = ?",
|
||||
Integer.class,
|
||||
registeredClient.getClientSecret());
|
||||
if (count != null && count > 0) {
|
||||
throw new IllegalArgumentException("Registered client must be unique. " +
|
||||
"Found duplicate client secret for identifier: " + registeredClient.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegisteredClient findById(String id) {
|
||||
Assert.hasText(id, "id cannot be empty");
|
||||
|
||||
@@ -34,7 +34,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
@@ -90,7 +90,7 @@ public class OAuth2AuthorizationServerConfiguration {
|
||||
@Bean
|
||||
RegisterMissingBeanPostProcessor registerMissingBeanPostProcessor() {
|
||||
RegisterMissingBeanPostProcessor postProcessor = new RegisterMissingBeanPostProcessor();
|
||||
postProcessor.addBeanDefinition(ProviderSettings.class, () -> ProviderSettings.builder().build());
|
||||
postProcessor.addBeanDefinition(AuthorizationServerSettings.class, () -> AuthorizationServerSettings.builder().build());
|
||||
return postProcessor;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.config.annotation.web.configurers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
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.security.web.util.UrlUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A {@code Filter} that associates the {@link AuthorizationServerContext} to the {@link AuthorizationServerContextHolder}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.2
|
||||
* @see AuthorizationServerContext
|
||||
* @see AuthorizationServerContextHolder
|
||||
* @see AuthorizationServerSettings
|
||||
*/
|
||||
final class AuthorizationServerContextFilter extends OncePerRequestFilter {
|
||||
private final AuthorizationServerSettings authorizationServerSettings;
|
||||
|
||||
AuthorizationServerContextFilter(AuthorizationServerSettings authorizationServerSettings) {
|
||||
Assert.notNull(authorizationServerSettings, "authorizationServerSettings cannot be null");
|
||||
this.authorizationServerSettings = authorizationServerSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
try {
|
||||
AuthorizationServerContext authorizationServerContext =
|
||||
new DefaultAuthorizationServerContext(
|
||||
() -> resolveIssuer(this.authorizationServerSettings, request),
|
||||
this.authorizationServerSettings);
|
||||
AuthorizationServerContextHolder.setContext(authorizationServerContext);
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
AuthorizationServerContextHolder.resetContext();
|
||||
}
|
||||
}
|
||||
|
||||
private static String resolveIssuer(AuthorizationServerSettings authorizationServerSettings, HttpServletRequest request) {
|
||||
return authorizationServerSettings.getIssuer() != null ?
|
||||
authorizationServerSettings.getIssuer() :
|
||||
getContextPath(request);
|
||||
}
|
||||
|
||||
private static String getContextPath(HttpServletRequest request) {
|
||||
// @formatter:off
|
||||
return UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
||||
.replacePath(request.getContextPath())
|
||||
.replaceQuery(null)
|
||||
.fragment(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static final class DefaultAuthorizationServerContext implements AuthorizationServerContext {
|
||||
private final Supplier<String> issuerSupplier;
|
||||
private final AuthorizationServerSettings authorizationServerSettings;
|
||||
|
||||
private DefaultAuthorizationServerContext(Supplier<String> issuerSupplier, AuthorizationServerSettings authorizationServerSettings) {
|
||||
this.issuerSupplier = issuerSupplier;
|
||||
this.authorizationServerSettings = authorizationServerSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIssuer() {
|
||||
return this.issuerSupplier.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationServerSettings getAuthorizationServerSettings() {
|
||||
return this.authorizationServerSettings;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.authorization.config.annotati
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -27,11 +28,18 @@ 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.settings.ProviderSettings;
|
||||
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;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
@@ -52,11 +60,14 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private AuthenticationConverter authorizationRequestConverter;
|
||||
private final List<AuthenticationConverter> authorizationRequestConverters = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationConverter>> authorizationRequestConvertersConsumer = (authorizationRequestConverters) -> {};
|
||||
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
|
||||
private AuthenticationSuccessHandler authorizationResponseHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
private String consentPage;
|
||||
private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authorizationCodeRequestAuthenticationValidator;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
@@ -66,14 +77,32 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request.
|
||||
* Adds an {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} or {@link OAuth2AuthorizationConsentAuthenticationToken}
|
||||
* used for authenticating the request.
|
||||
*
|
||||
* @param authorizationRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
* @param authorizationRequestConverter an {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationEndpointConfigurer authorizationRequestConverter(AuthenticationConverter authorizationRequestConverter) {
|
||||
this.authorizationRequestConverter = authorizationRequestConverter;
|
||||
Assert.notNull(authorizationRequestConverter, "authorizationRequestConverter cannot be null");
|
||||
this.authorizationRequestConverters.add(authorizationRequestConverter);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@code List} of default
|
||||
* and (optionally) added {@link #authorizationRequestConverter(AuthenticationConverter) AuthenticationConverter}'s
|
||||
* allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}.
|
||||
*
|
||||
* @param authorizationRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s
|
||||
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2AuthorizationEndpointConfigurer authorizationRequestConverters(
|
||||
Consumer<List<AuthenticationConverter>> authorizationRequestConvertersConsumer) {
|
||||
Assert.notNull(authorizationRequestConvertersConsumer, "authorizationRequestConvertersConsumer cannot be null");
|
||||
this.authorizationRequestConvertersConsumer = authorizationRequestConvertersConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -89,6 +118,22 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
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 OAuth2AuthorizationEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2AuthorizationEndpointConfigurer 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 OAuth2AuthorizationCodeRequestAuthenticationToken}
|
||||
* and returning the {@link OAuth2AuthorizationResponse Authorization Response}.
|
||||
@@ -132,7 +177,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
*
|
||||
* <ul>
|
||||
* <li>It must be an HTTP POST</li>
|
||||
* <li>It must be submitted to {@link ProviderSettings#getAuthorizationEndpoint()} ()}</li>
|
||||
* <li>It must be submitted to {@link AuthorizationServerSettings#getAuthorizationEndpoint()}</li>
|
||||
* <li>It must include the received {@code client_id} as an HTTP parameter</li>
|
||||
* <li>It must include the received {@code state} as an HTTP parameter</li>
|
||||
* <li>It must include the list of {@code scope}s the {@code Resource Owner}
|
||||
@@ -147,21 +192,30 @@ 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) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
this.requestMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher(
|
||||
providerSettings.getAuthorizationEndpoint(),
|
||||
authorizationServerSettings.getAuthorizationEndpoint(),
|
||||
HttpMethod.GET.name()),
|
||||
new AntPathRequestMatcher(
|
||||
providerSettings.getAuthorizationEndpoint(),
|
||||
authorizationServerSettings.getAuthorizationEndpoint(),
|
||||
HttpMethod.POST.name()));
|
||||
|
||||
List<AuthenticationProvider> authenticationProviders =
|
||||
!this.authenticationProviders.isEmpty() ?
|
||||
this.authenticationProviders :
|
||||
createDefaultAuthenticationProviders(httpSecurity);
|
||||
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)));
|
||||
}
|
||||
@@ -169,15 +223,19 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
@Override
|
||||
void configure(HttpSecurity httpSecurity) {
|
||||
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
|
||||
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
|
||||
new OAuth2AuthorizationEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.getAuthorizationEndpoint());
|
||||
if (this.authorizationRequestConverter != null) {
|
||||
authorizationEndpointFilter.setAuthenticationConverter(this.authorizationRequestConverter);
|
||||
authorizationServerSettings.getAuthorizationEndpoint());
|
||||
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
|
||||
if (!this.authorizationRequestConverters.isEmpty()) {
|
||||
authenticationConverters.addAll(0, this.authorizationRequestConverters);
|
||||
}
|
||||
this.authorizationRequestConvertersConsumer.accept(authenticationConverters);
|
||||
authorizationEndpointFilter.setAuthenticationConverter(
|
||||
new DelegatingAuthenticationConverter(authenticationConverters));
|
||||
if (this.authorizationResponseHandler != null) {
|
||||
authorizationEndpointFilter.setAuthenticationSuccessHandler(this.authorizationResponseHandler);
|
||||
}
|
||||
@@ -195,6 +253,15 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
|
||||
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
|
||||
|
||||
authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter());
|
||||
authenticationConverters.add(new OAuth2AuthorizationConsentAuthenticationConverter());
|
||||
|
||||
return authenticationConverters;
|
||||
}
|
||||
|
||||
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
@@ -203,8 +270,20 @@ 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 =
|
||||
new OAuth2AuthorizationConsentAuthenticationProvider(
|
||||
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
|
||||
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
|
||||
OAuth2ConfigurerUtils.getAuthorizationConsentService(httpSecurity));
|
||||
authenticationProviders.add(authorizationConsentAuthenticationProvider);
|
||||
|
||||
return authenticationProviders;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,15 +29,18 @@ 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.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.ProviderContextFilter;
|
||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.context.SecurityContextHolderFilter;
|
||||
@@ -55,6 +60,7 @@ import org.springframework.util.Assert;
|
||||
* @since 0.0.1
|
||||
* @see AbstractHttpConfigurer
|
||||
* @see OAuth2ClientAuthenticationConfigurer
|
||||
* @see OAuth2AuthorizationServerMetadataEndpointConfigurer
|
||||
* @see OAuth2AuthorizationEndpointConfigurer
|
||||
* @see OAuth2TokenEndpointConfigurer
|
||||
* @see OAuth2TokenIntrospectionEndpointConfigurer
|
||||
@@ -64,22 +70,13 @@ import org.springframework.util.Assert;
|
||||
* @see OAuth2AuthorizationService
|
||||
* @see OAuth2AuthorizationConsentService
|
||||
* @see NimbusJwkSetEndpointFilter
|
||||
* @see OAuth2AuthorizationServerMetadataEndpointFilter
|
||||
*/
|
||||
public final class OAuth2AuthorizationServerConfigurer
|
||||
extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer, HttpSecurity> {
|
||||
|
||||
private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = createConfigurers();
|
||||
private RequestMatcher jwkSetEndpointMatcher;
|
||||
private RequestMatcher authorizationServerMetadataEndpointMatcher;
|
||||
private final RequestMatcher endpointsMatcher = (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) ||
|
||||
this.authorizationServerMetadataEndpointMatcher.matches(request);
|
||||
private RequestMatcher endpointsMatcher;
|
||||
|
||||
|
||||
/**
|
||||
* Sets the repository of registered clients.
|
||||
@@ -118,14 +115,14 @@ public final class OAuth2AuthorizationServerConfigurer
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the provider settings.
|
||||
* Sets the authorization server settings.
|
||||
*
|
||||
* @param providerSettings the provider settings
|
||||
* @param authorizationServerSettings the authorization server settings
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer providerSettings(ProviderSettings providerSettings) {
|
||||
Assert.notNull(providerSettings, "providerSettings cannot be null");
|
||||
getBuilder().setSharedObject(ProviderSettings.class, providerSettings);
|
||||
public OAuth2AuthorizationServerConfigurer authorizationServerSettings(AuthorizationServerSettings authorizationServerSettings) {
|
||||
Assert.notNull(authorizationServerSettings, "authorizationServerSettings cannot be null");
|
||||
getBuilder().setSharedObject(AuthorizationServerSettings.class, authorizationServerSettings);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -153,6 +150,18 @@ public final class OAuth2AuthorizationServerConfigurer
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OAuth 2.0 Authorization Server Metadata Endpoint.
|
||||
*
|
||||
* @param authorizationServerMetadataEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2AuthorizationServerMetadataEndpointConfigurer}
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer authorizationServerMetadataEndpoint(Customizer<OAuth2AuthorizationServerMetadataEndpointConfigurer> authorizationServerMetadataEndpointCustomizer) {
|
||||
authorizationServerMetadataEndpointCustomizer.customize(getConfigurer(OAuth2AuthorizationServerMetadataEndpointConfigurer.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OAuth 2.0 Authorization Endpoint.
|
||||
*
|
||||
@@ -200,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;
|
||||
}
|
||||
|
||||
@@ -216,16 +230,43 @@ 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
|
||||
public void init(HttpSecurity httpSecurity) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
validateProviderSettings(providerSettings);
|
||||
initEndpointMatchers(providerSettings);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
validateAuthorizationServerSettings(authorizationServerSettings);
|
||||
|
||||
this.configurers.values().forEach(configurer -> configurer.init(httpSecurity));
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -243,31 +284,27 @@ public final class OAuth2AuthorizationServerConfigurer
|
||||
public void configure(HttpSecurity httpSecurity) {
|
||||
this.configurers.values().forEach(configurer -> configurer.configure(httpSecurity));
|
||||
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
|
||||
ProviderContextFilter providerContextFilter = new ProviderContextFilter(providerSettings);
|
||||
httpSecurity.addFilterAfter(postProcess(providerContextFilter), SecurityContextHolderFilter.class);
|
||||
AuthorizationServerContextFilter authorizationServerContextFilter = new AuthorizationServerContextFilter(authorizationServerSettings);
|
||||
httpSecurity.addFilterAfter(postProcess(authorizationServerContextFilter), SecurityContextHolderFilter.class);
|
||||
|
||||
JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(httpSecurity);
|
||||
if (jwkSource != null) {
|
||||
NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
|
||||
jwkSource, providerSettings.getJwkSetEndpoint());
|
||||
jwkSource, authorizationServerSettings.getJwkSetEndpoint());
|
||||
httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
|
||||
new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings);
|
||||
httpSecurity.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
}
|
||||
|
||||
private Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> createConfigurers() {
|
||||
Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
|
||||
configurers.put(OAuth2ClientAuthenticationConfigurer.class, new OAuth2ClientAuthenticationConfigurer(this::postProcess));
|
||||
configurers.put(OAuth2AuthorizationServerMetadataEndpointConfigurer.class, new OAuth2AuthorizationServerMetadataEndpointConfigurer(this::postProcess));
|
||||
configurers.put(OAuth2AuthorizationEndpointConfigurer.class, new OAuth2AuthorizationEndpointConfigurer(this::postProcess));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -276,22 +313,20 @@ 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 void initEndpointMatchers(ProviderSettings providerSettings) {
|
||||
this.jwkSetEndpointMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.getJwkSetEndpoint(), HttpMethod.GET.name());
|
||||
this.authorizationServerMetadataEndpointMatcher = new AntPathRequestMatcher(
|
||||
"/.well-known/oauth-authorization-server", HttpMethod.GET.name());
|
||||
}
|
||||
|
||||
private static void validateProviderSettings(ProviderSettings providerSettings) {
|
||||
if (providerSettings.getIssuer() != null) {
|
||||
private static void validateAuthorizationServerSettings(AuthorizationServerSettings authorizationServerSettings) {
|
||||
if (authorizationServerSettings.getIssuer() != null) {
|
||||
URI issuerUri;
|
||||
try {
|
||||
issuerUri = new URI(providerSettings.getIssuer());
|
||||
issuerUri = new URI(authorizationServerSettings.getIssuer());
|
||||
issuerUri.toURL();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException("issuer must be a valid URL", ex);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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.config.annotation.web.configurers;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
/**
|
||||
* Configurer for the OAuth 2.0 Authorization Server Metadata Endpoint.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see OAuth2AuthorizationServerConfigurer#authorizationServerMetadataEndpoint
|
||||
* @see OAuth2AuthorizationServerMetadataEndpointFilter
|
||||
*/
|
||||
public final class OAuth2AuthorizationServerMetadataEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer;
|
||||
private Consumer<OAuth2AuthorizationServerMetadata.Builder> defaultAuthorizationServerMetadataCustomizer;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
*/
|
||||
OAuth2AuthorizationServerMetadataEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder}
|
||||
* allowing the ability to customize the claims of the Authorization Server's configuration.
|
||||
*
|
||||
* @param authorizationServerMetadataCustomizer the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder}
|
||||
* @return the {@link OAuth2AuthorizationServerMetadataEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2AuthorizationServerMetadataEndpointConfigurer authorizationServerMetadataCustomizer(
|
||||
Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer) {
|
||||
this.authorizationServerMetadataCustomizer = authorizationServerMetadataCustomizer;
|
||||
return this;
|
||||
}
|
||||
|
||||
void addDefaultAuthorizationServerMetadataCustomizer(
|
||||
Consumer<OAuth2AuthorizationServerMetadata.Builder> defaultAuthorizationServerMetadataCustomizer) {
|
||||
this.defaultAuthorizationServerMetadataCustomizer =
|
||||
this.defaultAuthorizationServerMetadataCustomizer == null ?
|
||||
defaultAuthorizationServerMetadataCustomizer :
|
||||
this.defaultAuthorizationServerMetadataCustomizer.andThen(defaultAuthorizationServerMetadataCustomizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
void init(HttpSecurity httpSecurity) {
|
||||
this.requestMatcher = new AntPathRequestMatcher(
|
||||
"/.well-known/oauth-authorization-server", HttpMethod.GET.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
void configure(HttpSecurity httpSecurity) {
|
||||
OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
|
||||
new OAuth2AuthorizationServerMetadataEndpointFilter();
|
||||
Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer = getAuthorizationServerMetadataCustomizer();
|
||||
if (authorizationServerMetadataCustomizer != null) {
|
||||
authorizationServerMetadataEndpointFilter.setAuthorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer);
|
||||
}
|
||||
httpSecurity.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
}
|
||||
|
||||
private Consumer<OAuth2AuthorizationServerMetadata.Builder> getAuthorizationServerMetadataCustomizer() {
|
||||
Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer = null;
|
||||
if (this.defaultAuthorizationServerMetadataCustomizer != null || this.authorizationServerMetadataCustomizer != null) {
|
||||
if (this.defaultAuthorizationServerMetadataCustomizer != null) {
|
||||
authorizationServerMetadataCustomizer = this.defaultAuthorizationServerMetadataCustomizer;
|
||||
}
|
||||
if (this.authorizationServerMetadataCustomizer != null) {
|
||||
authorizationServerMetadataCustomizer =
|
||||
authorizationServerMetadataCustomizer == null ?
|
||||
this.authorizationServerMetadataCustomizer :
|
||||
authorizationServerMetadataCustomizer.andThen(this.authorizationServerMetadataCustomizer);
|
||||
}
|
||||
}
|
||||
return authorizationServerMetadataCustomizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
RequestMatcher getRequestMatcher() {
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.authorization.config.annotati
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -34,8 +35,13 @@ import org.springframework.security.oauth2.server.authorization.authentication.J
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.PublicClientAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretBasicAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretPostAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.JwtClientAssertionAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
@@ -55,8 +61,10 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private AuthenticationConverter authenticationConverter;
|
||||
private final List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer = (authenticationConverters) -> {};
|
||||
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
|
||||
private AuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
|
||||
@@ -68,14 +76,31 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
|
||||
* Adds an {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2ClientAuthenticationToken} used for authenticating the client.
|
||||
*
|
||||
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
|
||||
* @param authenticationConverter an {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
|
||||
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2ClientAuthenticationConfigurer authenticationConverter(AuthenticationConverter authenticationConverter) {
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
this.authenticationConverters.add(authenticationConverter);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@code List} of default
|
||||
* and (optionally) added {@link #authenticationConverter(AuthenticationConverter) AuthenticationConverter}'s
|
||||
* allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}.
|
||||
*
|
||||
* @param authenticationConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s
|
||||
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2ClientAuthenticationConfigurer authenticationConverters(
|
||||
Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer) {
|
||||
Assert.notNull(authenticationConvertersConsumer, "authenticationConvertersConsumer cannot be null");
|
||||
this.authenticationConvertersConsumer = authenticationConvertersConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -91,6 +116,22 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co
|
||||
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 OAuth2ClientAuthenticationConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2ClientAuthenticationConfigurer authenticationProviders(
|
||||
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
|
||||
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
|
||||
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationSuccessHandler} used for handling a successful client authentication
|
||||
* and associating the {@link OAuth2ClientAuthenticationToken} to the {@link SecurityContext}.
|
||||
@@ -117,22 +158,23 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co
|
||||
|
||||
@Override
|
||||
void init(HttpSecurity httpSecurity) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
this.requestMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher(
|
||||
providerSettings.getTokenEndpoint(),
|
||||
authorizationServerSettings.getTokenEndpoint(),
|
||||
HttpMethod.POST.name()),
|
||||
new AntPathRequestMatcher(
|
||||
providerSettings.getTokenIntrospectionEndpoint(),
|
||||
authorizationServerSettings.getTokenIntrospectionEndpoint(),
|
||||
HttpMethod.POST.name()),
|
||||
new AntPathRequestMatcher(
|
||||
providerSettings.getTokenRevocationEndpoint(),
|
||||
authorizationServerSettings.getTokenRevocationEndpoint(),
|
||||
HttpMethod.POST.name()));
|
||||
|
||||
List<AuthenticationProvider> authenticationProviders =
|
||||
!this.authenticationProviders.isEmpty() ?
|
||||
this.authenticationProviders :
|
||||
createDefaultAuthenticationProviders(httpSecurity);
|
||||
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)));
|
||||
}
|
||||
@@ -142,9 +184,13 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co
|
||||
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
|
||||
OAuth2ClientAuthenticationFilter clientAuthenticationFilter = new OAuth2ClientAuthenticationFilter(
|
||||
authenticationManager, this.requestMatcher);
|
||||
if (this.authenticationConverter != null) {
|
||||
clientAuthenticationFilter.setAuthenticationConverter(this.authenticationConverter);
|
||||
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
|
||||
if (!this.authenticationConverters.isEmpty()) {
|
||||
authenticationConverters.addAll(0, this.authenticationConverters);
|
||||
}
|
||||
this.authenticationConvertersConsumer.accept(authenticationConverters);
|
||||
clientAuthenticationFilter.setAuthenticationConverter(
|
||||
new DelegatingAuthenticationConverter(authenticationConverters));
|
||||
if (this.authenticationSuccessHandler != null) {
|
||||
clientAuthenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
|
||||
}
|
||||
@@ -159,7 +205,18 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
|
||||
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
|
||||
|
||||
authenticationConverters.add(new JwtClientAssertionAuthenticationConverter());
|
||||
authenticationConverters.add(new ClientSecretBasicAuthenticationConverter());
|
||||
authenticationConverters.add(new ClientSecretPostAuthenticationConverter());
|
||||
authenticationConverters.add(new PublicClientAuthenticationConverter());
|
||||
|
||||
return authenticationConverters;
|
||||
}
|
||||
|
||||
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
RegisteredClientRepository registeredClientRepository = OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity);
|
||||
|
||||
@@ -34,7 +34,7 @@ import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2Au
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
|
||||
@@ -171,13 +171,13 @@ final class OAuth2ConfigurerUtils {
|
||||
return getOptionalBean(httpSecurity, type);
|
||||
}
|
||||
|
||||
static ProviderSettings getProviderSettings(HttpSecurity httpSecurity) {
|
||||
ProviderSettings providerSettings = httpSecurity.getSharedObject(ProviderSettings.class);
|
||||
if (providerSettings == null) {
|
||||
providerSettings = getBean(httpSecurity, ProviderSettings.class);
|
||||
httpSecurity.setSharedObject(ProviderSettings.class, providerSettings);
|
||||
static AuthorizationServerSettings getAuthorizationServerSettings(HttpSecurity httpSecurity) {
|
||||
AuthorizationServerSettings authorizationServerSettings = httpSecurity.getSharedObject(AuthorizationServerSettings.class);
|
||||
if (authorizationServerSettings == null) {
|
||||
authorizationServerSettings = getBean(httpSecurity, AuthorizationServerSettings.class);
|
||||
httpSecurity.setSharedObject(AuthorizationServerSettings.class, authorizationServerSettings);
|
||||
}
|
||||
return providerSettings;
|
||||
return authorizationServerSettings;
|
||||
}
|
||||
|
||||
static <T> T getBean(HttpSecurity httpSecurity, Class<T> type) {
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -36,9 +36,13 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
@@ -57,8 +61,10 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private AuthenticationConverter accessTokenRequestConverter;
|
||||
private final List<AuthenticationProvider> authenticationProviders = new LinkedList<>();
|
||||
private final List<AuthenticationConverter> accessTokenRequestConverters = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationConverter>> accessTokenRequestConvertersConsumer = (accessTokenRequestConverters) -> {};
|
||||
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
|
||||
private AuthenticationSuccessHandler accessTokenResponseHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
|
||||
@@ -70,14 +76,31 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest}
|
||||
* Adds an {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2AuthorizationGrantAuthenticationToken} used for authenticating the authorization grant.
|
||||
*
|
||||
* @param accessTokenRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest}
|
||||
* @param accessTokenRequestConverter an {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest}
|
||||
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenEndpointConfigurer accessTokenRequestConverter(AuthenticationConverter accessTokenRequestConverter) {
|
||||
this.accessTokenRequestConverter = accessTokenRequestConverter;
|
||||
Assert.notNull(accessTokenRequestConverter, "accessTokenRequestConverter cannot be null");
|
||||
this.accessTokenRequestConverters.add(accessTokenRequestConverter);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@code List} of default
|
||||
* and (optionally) added {@link #accessTokenRequestConverter(AuthenticationConverter) AuthenticationConverter}'s
|
||||
* allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}.
|
||||
*
|
||||
* @param accessTokenRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s
|
||||
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2TokenEndpointConfigurer accessTokenRequestConverters(
|
||||
Consumer<List<AuthenticationConverter>> accessTokenRequestConvertersConsumer) {
|
||||
Assert.notNull(accessTokenRequestConvertersConsumer, "accessTokenRequestConvertersConsumer cannot be null");
|
||||
this.accessTokenRequestConvertersConsumer = accessTokenRequestConvertersConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -93,6 +116,22 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
|
||||
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 OAuth2TokenEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2TokenEndpointConfigurer 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 OAuth2AccessTokenAuthenticationToken}
|
||||
* and returning the {@link OAuth2AccessTokenResponse Access Token Response}.
|
||||
@@ -119,14 +158,15 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
|
||||
|
||||
@Override
|
||||
void init(HttpSecurity httpSecurity) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
this.requestMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.getTokenEndpoint(), HttpMethod.POST.name());
|
||||
authorizationServerSettings.getTokenEndpoint(), HttpMethod.POST.name());
|
||||
|
||||
List<AuthenticationProvider> authenticationProviders =
|
||||
!this.authenticationProviders.isEmpty() ?
|
||||
this.authenticationProviders :
|
||||
createDefaultAuthenticationProviders(httpSecurity);
|
||||
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)));
|
||||
}
|
||||
@@ -134,15 +174,19 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
|
||||
@Override
|
||||
void configure(HttpSecurity httpSecurity) {
|
||||
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
|
||||
OAuth2TokenEndpointFilter tokenEndpointFilter =
|
||||
new OAuth2TokenEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.getTokenEndpoint());
|
||||
if (this.accessTokenRequestConverter != null) {
|
||||
tokenEndpointFilter.setAuthenticationConverter(this.accessTokenRequestConverter);
|
||||
authorizationServerSettings.getTokenEndpoint());
|
||||
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
|
||||
if (!this.accessTokenRequestConverters.isEmpty()) {
|
||||
authenticationConverters.addAll(0, this.accessTokenRequestConverters);
|
||||
}
|
||||
this.accessTokenRequestConvertersConsumer.accept(authenticationConverters);
|
||||
tokenEndpointFilter.setAuthenticationConverter(
|
||||
new DelegatingAuthenticationConverter(authenticationConverters));
|
||||
if (this.accessTokenResponseHandler != null) {
|
||||
tokenEndpointFilter.setAuthenticationSuccessHandler(this.accessTokenResponseHandler);
|
||||
}
|
||||
@@ -157,7 +201,17 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
|
||||
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
|
||||
|
||||
authenticationConverters.add(new OAuth2AuthorizationCodeAuthenticationConverter());
|
||||
authenticationConverters.add(new OAuth2RefreshTokenAuthenticationConverter());
|
||||
authenticationConverters.add(new OAuth2ClientCredentialsAuthenticationConverter());
|
||||
|
||||
return authenticationConverters;
|
||||
}
|
||||
|
||||
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity);
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -31,8 +31,10 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenIntrospectionAuthenticationConverter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
@@ -45,14 +47,17 @@ import org.springframework.util.Assert;
|
||||
* Configurer for the OAuth 2.0 Token Introspection Endpoint.
|
||||
*
|
||||
* @author Gaurav Tiwari
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.3
|
||||
* @see OAuth2AuthorizationServerConfigurer#tokenIntrospectionEndpoint(Customizer)
|
||||
* @see OAuth2TokenIntrospectionEndpointFilter
|
||||
*/
|
||||
public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private AuthenticationConverter introspectionRequestConverter;
|
||||
private final List<AuthenticationProvider> authenticationProviders = new LinkedList<>();
|
||||
private final List<AuthenticationConverter> introspectionRequestConverters = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationConverter>> introspectionRequestConvertersConsumer = (introspectionRequestConverters) -> {};
|
||||
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
|
||||
private AuthenticationSuccessHandler introspectionResponseHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
|
||||
@@ -64,14 +69,31 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest}
|
||||
* Adds an {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2TokenIntrospectionAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @param introspectionRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest}
|
||||
* @param introspectionRequestConverter an {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest}
|
||||
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenIntrospectionEndpointConfigurer introspectionRequestConverter(AuthenticationConverter introspectionRequestConverter) {
|
||||
this.introspectionRequestConverter = introspectionRequestConverter;
|
||||
Assert.notNull(introspectionRequestConverter, "introspectionRequestConverter cannot be null");
|
||||
this.introspectionRequestConverters.add(introspectionRequestConverter);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@code List} of default
|
||||
* and (optionally) added {@link #introspectionRequestConverter(AuthenticationConverter) AuthenticationConverter}'s
|
||||
* allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}.
|
||||
*
|
||||
* @param introspectionRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s
|
||||
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2TokenIntrospectionEndpointConfigurer introspectionRequestConverters(
|
||||
Consumer<List<AuthenticationConverter>> introspectionRequestConvertersConsumer) {
|
||||
Assert.notNull(introspectionRequestConvertersConsumer, "introspectionRequestConvertersConsumer cannot be null");
|
||||
this.introspectionRequestConvertersConsumer = introspectionRequestConvertersConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -87,6 +109,22 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA
|
||||
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 OAuth2TokenIntrospectionEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2TokenIntrospectionEndpointConfigurer 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 OAuth2TokenIntrospectionAuthenticationToken}.
|
||||
*
|
||||
@@ -112,14 +150,15 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA
|
||||
|
||||
@Override
|
||||
void init(HttpSecurity httpSecurity) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
this.requestMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.getTokenIntrospectionEndpoint(), HttpMethod.POST.name());
|
||||
authorizationServerSettings.getTokenIntrospectionEndpoint(), HttpMethod.POST.name());
|
||||
|
||||
List<AuthenticationProvider> authenticationProviders =
|
||||
!this.authenticationProviders.isEmpty() ?
|
||||
this.authenticationProviders :
|
||||
createDefaultAuthenticationProviders(httpSecurity);
|
||||
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)));
|
||||
}
|
||||
@@ -127,14 +166,18 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA
|
||||
@Override
|
||||
void configure(HttpSecurity httpSecurity) {
|
||||
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
|
||||
OAuth2TokenIntrospectionEndpointFilter introspectionEndpointFilter =
|
||||
new OAuth2TokenIntrospectionEndpointFilter(
|
||||
authenticationManager, providerSettings.getTokenIntrospectionEndpoint());
|
||||
if (this.introspectionRequestConverter != null) {
|
||||
introspectionEndpointFilter.setAuthenticationConverter(this.introspectionRequestConverter);
|
||||
authenticationManager, authorizationServerSettings.getTokenIntrospectionEndpoint());
|
||||
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
|
||||
if (!this.introspectionRequestConverters.isEmpty()) {
|
||||
authenticationConverters.addAll(0, this.introspectionRequestConverters);
|
||||
}
|
||||
this.introspectionRequestConvertersConsumer.accept(authenticationConverters);
|
||||
introspectionEndpointFilter.setAuthenticationConverter(
|
||||
new DelegatingAuthenticationConverter(authenticationConverters));
|
||||
if (this.introspectionResponseHandler != null) {
|
||||
introspectionEndpointFilter.setAuthenticationSuccessHandler(this.introspectionResponseHandler);
|
||||
}
|
||||
@@ -149,7 +192,15 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
|
||||
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
|
||||
|
||||
authenticationConverters.add(new OAuth2TokenIntrospectionAuthenticationConverter());
|
||||
|
||||
return authenticationConverters;
|
||||
}
|
||||
|
||||
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -30,8 +30,10 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationAuthenticationConverter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
@@ -44,14 +46,17 @@ import org.springframework.util.Assert;
|
||||
* Configurer for the OAuth 2.0 Token Revocation Endpoint.
|
||||
*
|
||||
* @author Arfat Chaus
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.2
|
||||
* @see OAuth2AuthorizationServerConfigurer#tokenRevocationEndpoint
|
||||
* @see OAuth2TokenRevocationEndpointFilter
|
||||
*/
|
||||
public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private AuthenticationConverter revocationRequestConverter;
|
||||
private final List<AuthenticationProvider> authenticationProviders = new LinkedList<>();
|
||||
private final List<AuthenticationConverter> revocationRequestConverters = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationConverter>> revocationRequestConvertersConsumer = (revocationRequestConverters) -> {};
|
||||
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
|
||||
private AuthenticationSuccessHandler revocationResponseHandler;
|
||||
private AuthenticationFailureHandler errorResponseHandler;
|
||||
|
||||
@@ -63,14 +68,31 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract a Revoke Token Request from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the client.
|
||||
* Adds an {@link AuthenticationConverter} used when attempting to extract a Revoke Token Request from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @param revocationRequestConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
|
||||
* @param revocationRequestConverter an {@link AuthenticationConverter} used when attempting to extract a Revoke Token Request from {@link HttpServletRequest}
|
||||
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OAuth2TokenRevocationEndpointConfigurer revocationRequestConverter(AuthenticationConverter revocationRequestConverter) {
|
||||
this.revocationRequestConverter = revocationRequestConverter;
|
||||
Assert.notNull(revocationRequestConverter, "revocationRequestConverter cannot be null");
|
||||
this.revocationRequestConverters.add(revocationRequestConverter);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@code List} of default
|
||||
* and (optionally) added {@link #revocationRequestConverter(AuthenticationConverter) AuthenticationConverter}'s
|
||||
* allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}.
|
||||
*
|
||||
* @param revocationRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s
|
||||
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2TokenRevocationEndpointConfigurer revocationRequestConverters(
|
||||
Consumer<List<AuthenticationConverter>> revocationRequestConvertersConsumer) {
|
||||
Assert.notNull(revocationRequestConvertersConsumer, "revocationRequestConvertersConsumer cannot be null");
|
||||
this.revocationRequestConvertersConsumer = revocationRequestConvertersConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -86,6 +108,22 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth
|
||||
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 OAuth2TokenRevocationEndpointConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OAuth2TokenRevocationEndpointConfigurer 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 OAuth2TokenRevocationAuthenticationToken}.
|
||||
*
|
||||
@@ -111,14 +149,15 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth
|
||||
|
||||
@Override
|
||||
void init(HttpSecurity httpSecurity) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
this.requestMatcher = new AntPathRequestMatcher(
|
||||
providerSettings.getTokenRevocationEndpoint(), HttpMethod.POST.name());
|
||||
authorizationServerSettings.getTokenRevocationEndpoint(), HttpMethod.POST.name());
|
||||
|
||||
List<AuthenticationProvider> authenticationProviders =
|
||||
!this.authenticationProviders.isEmpty() ?
|
||||
this.authenticationProviders :
|
||||
createDefaultAuthenticationProviders(httpSecurity);
|
||||
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)));
|
||||
}
|
||||
@@ -126,14 +165,18 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth
|
||||
@Override
|
||||
void configure(HttpSecurity httpSecurity) {
|
||||
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
|
||||
OAuth2TokenRevocationEndpointFilter revocationEndpointFilter =
|
||||
new OAuth2TokenRevocationEndpointFilter(
|
||||
authenticationManager, providerSettings.getTokenRevocationEndpoint());
|
||||
if (this.revocationRequestConverter != null) {
|
||||
revocationEndpointFilter.setAuthenticationConverter(this.revocationRequestConverter);
|
||||
authenticationManager, authorizationServerSettings.getTokenRevocationEndpoint());
|
||||
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
|
||||
if (!this.revocationRequestConverters.isEmpty()) {
|
||||
authenticationConverters.addAll(0, this.revocationRequestConverters);
|
||||
}
|
||||
this.revocationRequestConvertersConsumer.accept(authenticationConverters);
|
||||
revocationEndpointFilter.setAuthenticationConverter(
|
||||
new DelegatingAuthenticationConverter(authenticationConverters));
|
||||
if (this.revocationResponseHandler != null) {
|
||||
revocationEndpointFilter.setAuthenticationSuccessHandler(this.revocationResponseHandler);
|
||||
}
|
||||
@@ -148,7 +191,15 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
|
||||
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
|
||||
|
||||
authenticationConverters.add(new OAuth2TokenRevocationAuthenticationConverter());
|
||||
|
||||
return authenticationConverters;
|
||||
}
|
||||
|
||||
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
|
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
|
||||
|
||||
OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =
|
||||
|
||||
@@ -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.settings.ProviderSettings;
|
||||
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,31 +70,133 @@ 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) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
String clientRegistrationEndpointUri = authorizationServerSettings.getOidcClientRegistrationEndpoint();
|
||||
this.requestMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher(providerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.POST.name()),
|
||||
new AntPathRequestMatcher(providerSettings.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
|
||||
void configure(HttpSecurity httpSecurity) {
|
||||
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
|
||||
OidcClientRegistrationEndpointFilter oidcClientRegistrationEndpointFilter =
|
||||
new OidcClientRegistrationEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.getOidcClientRegistrationEndpoint());
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,16 +20,15 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
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.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* Configurer for OpenID Connect 1.0 support.
|
||||
@@ -37,9 +36,9 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.0
|
||||
* @see OAuth2AuthorizationServerConfigurer#oidc
|
||||
* @see OidcProviderConfigurationEndpointConfigurer
|
||||
* @see OidcClientRegistrationEndpointConfigurer
|
||||
* @see OidcUserInfoEndpointConfigurer
|
||||
* @see OidcProviderConfigurationEndpointFilter
|
||||
*/
|
||||
public final class OidcConfigurer extends AbstractOAuth2Configurer {
|
||||
private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
|
||||
@@ -50,9 +49,22 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer {
|
||||
*/
|
||||
OidcConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
addConfigurer(OidcProviderConfigurationEndpointConfigurer.class, new OidcProviderConfigurationEndpointConfigurer(objectPostProcessor));
|
||||
addConfigurer(OidcUserInfoEndpointConfigurer.class, new OidcUserInfoEndpointConfigurer(objectPostProcessor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OpenID Connect 1.0 Provider Configuration Endpoint.
|
||||
*
|
||||
* @param providerConfigurationEndpointCustomizer the {@link Customizer} providing access to the {@link OidcProviderConfigurationEndpointConfigurer}
|
||||
* @return the {@link OidcConfigurer} for further configuration
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public OidcConfigurer providerConfigurationEndpoint(Customizer<OidcProviderConfigurationEndpointConfigurer> providerConfigurationEndpointCustomizer) {
|
||||
providerConfigurationEndpointCustomizer.customize(getConfigurer(OidcProviderConfigurationEndpointConfigurer.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OpenID Connect Dynamic Client Registration 1.0 Endpoint.
|
||||
*
|
||||
@@ -84,40 +96,36 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer {
|
||||
|
||||
@Override
|
||||
void init(HttpSecurity httpSecurity) {
|
||||
OidcUserInfoEndpointConfigurer userInfoEndpointConfigurer =
|
||||
getConfigurer(OidcUserInfoEndpointConfigurer.class);
|
||||
userInfoEndpointConfigurer.init(httpSecurity);
|
||||
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
|
||||
getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
|
||||
if (clientRegistrationEndpointConfigurer != null) {
|
||||
clientRegistrationEndpointConfigurer.init(httpSecurity);
|
||||
}
|
||||
|
||||
List<RequestMatcher> requestMatchers = new ArrayList<>();
|
||||
requestMatchers.add(new AntPathRequestMatcher(
|
||||
"/.well-known/openid-configuration", HttpMethod.GET.name()));
|
||||
requestMatchers.add(userInfoEndpointConfigurer.getRequestMatcher());
|
||||
if (clientRegistrationEndpointConfigurer != null) {
|
||||
requestMatchers.add(clientRegistrationEndpointConfigurer.getRequestMatcher());
|
||||
}
|
||||
this.configurers.values().forEach(configurer -> {
|
||||
configurer.init(httpSecurity);
|
||||
requestMatchers.add(configurer.getRequestMatcher());
|
||||
});
|
||||
this.requestMatcher = new OrRequestMatcher(requestMatchers);
|
||||
}
|
||||
|
||||
@Override
|
||||
void configure(HttpSecurity httpSecurity) {
|
||||
OidcUserInfoEndpointConfigurer userInfoEndpointConfigurer =
|
||||
getConfigurer(OidcUserInfoEndpointConfigurer.class);
|
||||
userInfoEndpointConfigurer.configure(httpSecurity);
|
||||
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
|
||||
getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
|
||||
if (clientRegistrationEndpointConfigurer != null) {
|
||||
clientRegistrationEndpointConfigurer.configure(httpSecurity);
|
||||
OidcProviderConfigurationEndpointConfigurer providerConfigurationEndpointConfigurer =
|
||||
getConfigurer(OidcProviderConfigurationEndpointConfigurer.class);
|
||||
|
||||
providerConfigurationEndpointConfigurer
|
||||
.addDefaultProviderConfigurationCustomizer((builder) -> {
|
||||
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
|
||||
String issuer = authorizationServerContext.getIssuer();
|
||||
AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings();
|
||||
|
||||
String clientRegistrationEndpoint = UriComponentsBuilder.fromUriString(issuer)
|
||||
.path(authorizationServerSettings.getOidcClientRegistrationEndpoint()).build().toUriString();
|
||||
|
||||
builder.clientRegistrationEndpoint(clientRegistrationEndpoint);
|
||||
});
|
||||
}
|
||||
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
|
||||
new OidcProviderConfigurationEndpointFilter(providerSettings);
|
||||
httpSecurity.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
this.configurers.values().forEach(configurer -> configurer.configure(httpSecurity));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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.config.annotation.web.configurers;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
|
||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
/**
|
||||
* Configurer for the OpenID Connect 1.0 Provider Configuration Endpoint.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see OidcConfigurer#providerConfigurationEndpoint
|
||||
* @see OidcProviderConfigurationEndpointFilter
|
||||
*/
|
||||
public final class OidcProviderConfigurationEndpointConfigurer extends AbstractOAuth2Configurer {
|
||||
private RequestMatcher requestMatcher;
|
||||
private Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer;
|
||||
private Consumer<OidcProviderConfiguration.Builder> defaultProviderConfigurationCustomizer;
|
||||
|
||||
/**
|
||||
* Restrict for internal use only.
|
||||
*/
|
||||
OidcProviderConfigurationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
super(objectPostProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@link OidcProviderConfiguration.Builder}
|
||||
* allowing the ability to customize the claims of the OpenID Provider's configuration.
|
||||
*
|
||||
* @param providerConfigurationCustomizer the {@code Consumer} providing access to the {@link OidcProviderConfiguration.Builder}
|
||||
* @return the {@link OidcProviderConfigurationEndpointConfigurer} for further configuration
|
||||
*/
|
||||
public OidcProviderConfigurationEndpointConfigurer providerConfigurationCustomizer(
|
||||
Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer) {
|
||||
this.providerConfigurationCustomizer = providerConfigurationCustomizer;
|
||||
return this;
|
||||
}
|
||||
|
||||
void addDefaultProviderConfigurationCustomizer(
|
||||
Consumer<OidcProviderConfiguration.Builder> defaultProviderConfigurationCustomizer) {
|
||||
this.defaultProviderConfigurationCustomizer =
|
||||
this.defaultProviderConfigurationCustomizer == null ?
|
||||
defaultProviderConfigurationCustomizer :
|
||||
this.defaultProviderConfigurationCustomizer.andThen(defaultProviderConfigurationCustomizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
void init(HttpSecurity httpSecurity) {
|
||||
this.requestMatcher = new AntPathRequestMatcher(
|
||||
"/.well-known/openid-configuration", HttpMethod.GET.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
void configure(HttpSecurity httpSecurity) {
|
||||
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
|
||||
new OidcProviderConfigurationEndpointFilter();
|
||||
Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer = getProviderConfigurationCustomizer();
|
||||
if (providerConfigurationCustomizer != null) {
|
||||
oidcProviderConfigurationEndpointFilter.setProviderConfigurationCustomizer(providerConfigurationCustomizer);
|
||||
}
|
||||
httpSecurity.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
|
||||
}
|
||||
|
||||
private Consumer<OidcProviderConfiguration.Builder> getProviderConfigurationCustomizer() {
|
||||
Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer = null;
|
||||
if (this.defaultProviderConfigurationCustomizer != null || this.providerConfigurationCustomizer != null) {
|
||||
if (this.defaultProviderConfigurationCustomizer != null) {
|
||||
providerConfigurationCustomizer = this.defaultProviderConfigurationCustomizer;
|
||||
}
|
||||
if (this.providerConfigurationCustomizer != null) {
|
||||
providerConfigurationCustomizer =
|
||||
providerConfigurationCustomizer == null ?
|
||||
this.providerConfigurationCustomizer :
|
||||
providerConfigurationCustomizer.andThen(this.providerConfigurationCustomizer);
|
||||
}
|
||||
}
|
||||
return providerConfigurationCustomizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
RequestMatcher getRequestMatcher() {
|
||||
return this.requestMatcher;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,35 +15,57 @@
|
||||
*/
|
||||
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;
|
||||
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.web.OidcUserInfoEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
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,37 +176,51 @@ 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
void init(HttpSecurity httpSecurity) {
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
String userInfoEndpointUri = providerSettings.getOidcUserInfoEndpoint();
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
String userInfoEndpointUri = authorizationServerSettings.getOidcUserInfoEndpoint();
|
||||
this.requestMatcher = new OrRequestMatcher(
|
||||
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
|
||||
void configure(HttpSecurity httpSecurity) {
|
||||
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
|
||||
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(httpSecurity);
|
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
|
||||
|
||||
OidcUserInfoEndpointFilter oidcUserInfoEndpointFilter =
|
||||
new OidcUserInfoEndpointFilter(
|
||||
authenticationManager,
|
||||
providerSettings.getOidcUserInfoEndpoint());
|
||||
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,44 @@
|
||||
/*
|
||||
* 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.context;
|
||||
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
|
||||
/**
|
||||
* A context that holds information of the Authorization Server runtime environment.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.2
|
||||
* @see AuthorizationServerSettings
|
||||
* @see AuthorizationServerContextHolder
|
||||
*/
|
||||
public interface AuthorizationServerContext {
|
||||
|
||||
/**
|
||||
* Returns the {@code URL} of the Authorization Server's issuer identifier.
|
||||
*
|
||||
* @return the {@code URL} of the Authorization Server's issuer identifier
|
||||
*/
|
||||
String getIssuer();
|
||||
|
||||
/**
|
||||
* Returns the {@link AuthorizationServerSettings}.
|
||||
*
|
||||
* @return the {@link AuthorizationServerSettings}
|
||||
*/
|
||||
AuthorizationServerSettings getAuthorizationServerSettings();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.context;
|
||||
|
||||
/**
|
||||
* A holder of the {@link AuthorizationServerContext} that associates it with the current thread using a {@code ThreadLocal}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.2
|
||||
* @see AuthorizationServerContext
|
||||
*/
|
||||
public final class AuthorizationServerContextHolder {
|
||||
private static final ThreadLocal<AuthorizationServerContext> holder = new ThreadLocal<>();
|
||||
|
||||
private AuthorizationServerContextHolder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link AuthorizationServerContext} bound to the current thread.
|
||||
*
|
||||
* @return the {@link AuthorizationServerContext}
|
||||
*/
|
||||
public static AuthorizationServerContext getContext() {
|
||||
return holder.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the given {@link AuthorizationServerContext} to the current thread.
|
||||
*
|
||||
* @param authorizationServerContext the {@link AuthorizationServerContext}
|
||||
*/
|
||||
public static void setContext(AuthorizationServerContext authorizationServerContext) {
|
||||
if (authorizationServerContext == null) {
|
||||
resetContext();
|
||||
} else {
|
||||
holder.set(authorizationServerContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the {@link AuthorizationServerContext} bound to the current thread.
|
||||
*/
|
||||
public static void resetContext() {
|
||||
holder.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* 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.context;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A context that holds information of the Provider.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.2
|
||||
* @see ProviderSettings
|
||||
* @see ProviderContextHolder
|
||||
*/
|
||||
public final class ProviderContext {
|
||||
private final ProviderSettings providerSettings;
|
||||
private final Supplier<String> issuerSupplier;
|
||||
|
||||
/**
|
||||
* Constructs a {@code ProviderContext} using the provided parameters.
|
||||
*
|
||||
* @param providerSettings the provider settings
|
||||
* @param issuerSupplier a {@code Supplier} for the {@code URL} of the Provider's issuer identifier
|
||||
*/
|
||||
public ProviderContext(ProviderSettings providerSettings, @Nullable Supplier<String> issuerSupplier) {
|
||||
Assert.notNull(providerSettings, "providerSettings cannot be null");
|
||||
this.providerSettings = providerSettings;
|
||||
this.issuerSupplier = issuerSupplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ProviderSettings}.
|
||||
*
|
||||
* @return the {@link ProviderSettings}
|
||||
*/
|
||||
public ProviderSettings getProviderSettings() {
|
||||
return this.providerSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code URL} of the Provider's issuer identifier.
|
||||
* The issuer identifier is resolved from the constructor parameter {@code Supplier<String>}
|
||||
* or if not provided then defaults to {@link ProviderSettings#getIssuer()}.
|
||||
*
|
||||
* @return the {@code URL} of the Provider's issuer identifier
|
||||
*/
|
||||
public String getIssuer() {
|
||||
return this.issuerSupplier != null ?
|
||||
this.issuerSupplier.get() :
|
||||
getProviderSettings().getIssuer();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* 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.context;
|
||||
|
||||
import org.springframework.security.oauth2.server.authorization.web.ProviderContextFilter;
|
||||
|
||||
/**
|
||||
* A holder of {@link ProviderContext} that associates it with the current thread using a {@code ThreadLocal}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.2
|
||||
* @see ProviderContext
|
||||
* @see ProviderContextFilter
|
||||
*/
|
||||
public final class ProviderContextHolder {
|
||||
private static final ThreadLocal<ProviderContext> holder = new ThreadLocal<>();
|
||||
|
||||
private ProviderContextHolder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ProviderContext} bound to the current thread.
|
||||
*
|
||||
* @return the {@link ProviderContext}
|
||||
*/
|
||||
public static ProviderContext getProviderContext() {
|
||||
return holder.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the given {@link ProviderContext} to the current thread.
|
||||
*
|
||||
* @param providerContext the {@link ProviderContext}
|
||||
*/
|
||||
public static void setProviderContext(ProviderContext providerContext) {
|
||||
if (providerContext == null) {
|
||||
resetProviderContext();
|
||||
} else {
|
||||
holder.set(providerContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the {@link ProviderContext} bound to the current thread.
|
||||
*/
|
||||
public static void resetProviderContext() {
|
||||
holder.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
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 Log logger = LogFactory.getLog(getClass());
|
||||
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);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved authorization with access 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);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated client configuration request parameters");
|
||||
}
|
||||
|
||||
OidcClientRegistration clientRegistration = this.clientRegistrationConverter.convert(registeredClient);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated client configuration request");
|
||||
}
|
||||
|
||||
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,14 @@ 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.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
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,8 +54,7 @@ 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.ProviderContext;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
|
||||
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;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
@@ -62,10 +66,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 +77,19 @@ 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 Log logger = LogFactory.getLog(getClass());
|
||||
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 +107,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 +116,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,21 +132,23 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
}
|
||||
|
||||
String accessTokenValue = accessTokenAuthentication.getToken().getTokenValue();
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(
|
||||
accessTokenValue, OAuth2TokenType.ACCESS_TOKEN);
|
||||
if (authorization == null) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved authorization with initial access token");
|
||||
}
|
||||
|
||||
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
|
||||
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 +156,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,22 +178,39 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
throwInvalidClientRegistration("invalid_client_metadata", OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
|
||||
}
|
||||
|
||||
RegisteredClient registeredClient = createClient(clientRegistrationAuthentication.getClientRegistration());
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated client registration request parameters");
|
||||
}
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientConverter.convert(clientRegistrationAuthentication.getClientRegistration());
|
||||
this.registeredClientRepository.save(registeredClient);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Saved registered client");
|
||||
}
|
||||
|
||||
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)
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Saved authorization with invalidated initial access token");
|
||||
}
|
||||
|
||||
Map<String, Object> clientRegistrationClaims = this.clientRegistrationConverter.convert(registeredClient).getClaims();
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.withClaims(clientRegistrationClaims)
|
||||
.registrationAccessToken(registeredClientAuthorization.getAccessToken().getToken().getTokenValue())
|
||||
.build();
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated client registration request");
|
||||
}
|
||||
|
||||
return new OidcClientRegistrationAuthenticationToken(
|
||||
(Authentication) clientRegistrationAuthentication.getPrincipal(), clientRegistration);
|
||||
}
|
||||
@@ -205,14 +220,14 @@ 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
|
||||
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(clientPrincipal)
|
||||
.providerContext(ProviderContextHolder.getProviderContext())
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
@@ -225,6 +240,11 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
"The token generator failed to generate the registration access token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Generated registration access token");
|
||||
}
|
||||
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
registrationAccessToken.getTokenValue(), registrationAccessToken.getIssuedAt(),
|
||||
registrationAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
@@ -246,69 +266,13 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Saved authorization with registration access token");
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
ProviderContext providerContext = ProviderContextHolder.getProviderContext();
|
||||
String registrationClientUri = UriComponentsBuilder.fromUriString(providerContext.getIssuer())
|
||||
.path(providerContext.getProviderSettings().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 +330,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 +338,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;
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
@@ -31,12 +34,27 @@ import org.springframework.util.Assert;
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.1
|
||||
* @see OAuth2AuthenticationContext
|
||||
* @see OidcUserInfo
|
||||
* @see OidcUserInfoAuthenticationProvider#setUserInfoMapper(Function)
|
||||
*/
|
||||
public final class OidcUserInfoAuthenticationContext extends OAuth2AuthenticationContext {
|
||||
public final class OidcUserInfoAuthenticationContext implements OAuth2AuthenticationContext {
|
||||
private final Map<Object, Object> context;
|
||||
|
||||
private OidcUserInfoAuthenticationContext(Map<Object, Object> context) {
|
||||
super(context);
|
||||
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
return this.context.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,9 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
@@ -48,6 +51,7 @@ import org.springframework.util.Assert;
|
||||
* @see <a href="https://openid.net/specs/openid-connect-core-1_0.html#UserInfo">5.3. UserInfo Endpoint</a>
|
||||
*/
|
||||
public final class OidcUserInfoAuthenticationProvider implements AuthenticationProvider {
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper = new DefaultOidcUserInfoMapper();
|
||||
|
||||
@@ -82,6 +86,10 @@ public final class OidcUserInfoAuthenticationProvider implements AuthenticationP
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Retrieved authorization with access token");
|
||||
}
|
||||
|
||||
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
|
||||
if (!authorizedAccessToken.isActive()) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
@@ -96,6 +104,10 @@ public final class OidcUserInfoAuthenticationProvider implements AuthenticationP
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated user info request");
|
||||
}
|
||||
|
||||
OidcUserInfoAuthenticationContext authenticationContext =
|
||||
OidcUserInfoAuthenticationContext.with(userInfoAuthentication)
|
||||
.accessToken(authorizedAccessToken.getToken())
|
||||
@@ -103,6 +115,10 @@ public final class OidcUserInfoAuthenticationProvider implements AuthenticationP
|
||||
.build();
|
||||
OidcUserInfo userInfo = this.userInfoMapper.apply(authenticationContext);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authenticated user info request");
|
||||
}
|
||||
|
||||
return new OidcUserInfoAuthenticationToken(accessTokenAuthentication, userInfo);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,13 +22,14 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
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 +37,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 +54,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 +79,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 +106,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 +118,7 @@ public final class OidcClientRegistrationEndpointFilter extends OncePerRequestFi
|
||||
return StringUtils.hasText(clientId);
|
||||
};
|
||||
|
||||
return new AndRequestMatcher(configureClientGetMatcher, clientIdMatcher);
|
||||
return new AndRequestMatcher(clientConfigurationGetMatcher, clientIdMatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,57 +131,84 @@ 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());
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Client registration request failed: %s", ex.getError()), ex);
|
||||
}
|
||||
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);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(error.getDescription(), ex);
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -32,10 +32,11 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
|
||||
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.OidcProviderConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcProviderConfigurationHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -46,9 +47,10 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
* A {@code Filter} that processes OpenID Provider Configuration Requests.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see OidcProviderConfiguration
|
||||
* @see ProviderSettings
|
||||
* @see AuthorizationServerSettings
|
||||
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">4.1. OpenID Provider Configuration Request</a>
|
||||
*/
|
||||
public final class OidcProviderConfigurationEndpointFilter extends OncePerRequestFilter {
|
||||
@@ -57,18 +59,23 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
|
||||
*/
|
||||
private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration";
|
||||
|
||||
private final ProviderSettings providerSettings;
|
||||
private final RequestMatcher requestMatcher;
|
||||
private final RequestMatcher requestMatcher = new AntPathRequestMatcher(
|
||||
DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI,
|
||||
HttpMethod.GET.name());
|
||||
private final OidcProviderConfigurationHttpMessageConverter providerConfigurationHttpMessageConverter =
|
||||
new OidcProviderConfigurationHttpMessageConverter();
|
||||
private Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer = (providerConfiguration) -> {};
|
||||
|
||||
public OidcProviderConfigurationEndpointFilter(ProviderSettings providerSettings) {
|
||||
Assert.notNull(providerSettings, "providerSettings cannot be null");
|
||||
this.providerSettings = providerSettings;
|
||||
this.requestMatcher = new AntPathRequestMatcher(
|
||||
DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI,
|
||||
HttpMethod.GET.name()
|
||||
);
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@link OidcProviderConfiguration.Builder}
|
||||
* allowing the ability to customize the claims of the OpenID Provider's configuration.
|
||||
*
|
||||
* @param providerConfigurationCustomizer the {@code Consumer} providing access to the {@link OidcProviderConfiguration.Builder}
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public void setProviderConfigurationCustomizer(Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer) {
|
||||
Assert.notNull(providerConfigurationCustomizer, "providerConfigurationCustomizer cannot be null");
|
||||
this.providerConfigurationCustomizer = providerConfigurationCustomizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,31 +87,34 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
|
||||
return;
|
||||
}
|
||||
|
||||
String issuer = ProviderContextHolder.getProviderContext().getIssuer();
|
||||
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
|
||||
String issuer = authorizationServerContext.getIssuer();
|
||||
AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings();
|
||||
|
||||
OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.builder()
|
||||
OidcProviderConfiguration.Builder providerConfiguration = OidcProviderConfiguration.builder()
|
||||
.issuer(issuer)
|
||||
.authorizationEndpoint(asUrl(issuer, this.providerSettings.getAuthorizationEndpoint()))
|
||||
.tokenEndpoint(asUrl(issuer, this.providerSettings.getTokenEndpoint()))
|
||||
.authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint()))
|
||||
.tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint()))
|
||||
.tokenEndpointAuthenticationMethods(clientAuthenticationMethods())
|
||||
.jwkSetUrl(asUrl(issuer, this.providerSettings.getJwkSetEndpoint()))
|
||||
.userInfoEndpoint(asUrl(issuer, this.providerSettings.getOidcUserInfoEndpoint()))
|
||||
.jwkSetUrl(asUrl(issuer, authorizationServerSettings.getJwkSetEndpoint()))
|
||||
.userInfoEndpoint(asUrl(issuer, authorizationServerSettings.getOidcUserInfoEndpoint()))
|
||||
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
|
||||
.tokenRevocationEndpoint(asUrl(issuer, this.providerSettings.getTokenRevocationEndpoint()))
|
||||
.tokenRevocationEndpoint(asUrl(issuer, authorizationServerSettings.getTokenRevocationEndpoint()))
|
||||
.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
|
||||
.tokenIntrospectionEndpoint(asUrl(issuer, this.providerSettings.getTokenIntrospectionEndpoint()))
|
||||
.tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint()))
|
||||
.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
|
||||
.subjectType("public")
|
||||
.idTokenSigningAlgorithm(SignatureAlgorithm.RS256.getName())
|
||||
.scope(OidcScopes.OPENID)
|
||||
.build();
|
||||
.scope(OidcScopes.OPENID);
|
||||
|
||||
this.providerConfigurationCustomizer.accept(providerConfiguration);
|
||||
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
this.providerConfigurationHttpMessageConverter.write(
|
||||
providerConfiguration, MediaType.APPLICATION_JSON, httpResponse);
|
||||
providerConfiguration.build(), MediaType.APPLICATION_JSON, httpResponse);
|
||||
}
|
||||
|
||||
private static Consumer<List<String>> clientAuthenticationMethods() {
|
||||
@@ -119,4 +129,5 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
|
||||
private static String asUrl(String issuer, String endpoint) {
|
||||
return UriComponentsBuilder.fromUriString(issuer).path(endpoint).build().toUriString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,20 +22,26 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
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 +53,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 +68,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 +110,83 @@ 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());
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("User info request failed: %s", ex.getError()), ex);
|
||||
}
|
||||
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);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(error.getDescription(), ex);
|
||||
}
|
||||
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 +197,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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,90 +20,90 @@ import java.util.Map;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A facility for provider configuration settings.
|
||||
* A facility for authorization server configuration settings.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.0
|
||||
* @see AbstractSettings
|
||||
* @see ConfigurationSettingNames.Provider
|
||||
* @see ConfigurationSettingNames.AuthorizationServer
|
||||
*/
|
||||
public final class ProviderSettings extends AbstractSettings {
|
||||
public final class AuthorizationServerSettings extends AbstractSettings {
|
||||
|
||||
private ProviderSettings(Map<String, Object> settings) {
|
||||
private AuthorizationServerSettings(Map<String, Object> settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the Provider's Issuer Identifier
|
||||
* Returns the URL of the Authorization Server's Issuer Identifier
|
||||
*
|
||||
* @return the URL of the Provider's Issuer Identifier
|
||||
* @return the URL of the Authorization Server's Issuer Identifier
|
||||
*/
|
||||
public String getIssuer() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.ISSUER);
|
||||
return getSetting(ConfigurationSettingNames.AuthorizationServer.ISSUER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider's OAuth 2.0 Authorization endpoint. The default is {@code /oauth2/authorize}.
|
||||
* Returns the OAuth 2.0 Authorization endpoint. The default is {@code /oauth2/authorize}.
|
||||
*
|
||||
* @return the Authorization endpoint
|
||||
*/
|
||||
public String getAuthorizationEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.AUTHORIZATION_ENDPOINT);
|
||||
return getSetting(ConfigurationSettingNames.AuthorizationServer.AUTHORIZATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider's OAuth 2.0 Token endpoint. The default is {@code /oauth2/token}.
|
||||
* Returns the OAuth 2.0 Token endpoint. The default is {@code /oauth2/token}.
|
||||
*
|
||||
* @return the Token endpoint
|
||||
*/
|
||||
public String getTokenEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.TOKEN_ENDPOINT);
|
||||
return getSetting(ConfigurationSettingNames.AuthorizationServer.TOKEN_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider's JWK Set endpoint. The default is {@code /oauth2/jwks}.
|
||||
* Returns the JWK Set endpoint. The default is {@code /oauth2/jwks}.
|
||||
*
|
||||
* @return the JWK Set endpoint
|
||||
*/
|
||||
public String getJwkSetEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT);
|
||||
return getSetting(ConfigurationSettingNames.AuthorizationServer.JWK_SET_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider's OAuth 2.0 Token Revocation endpoint. The default is {@code /oauth2/revoke}.
|
||||
* Returns the OAuth 2.0 Token Revocation endpoint. The default is {@code /oauth2/revoke}.
|
||||
*
|
||||
* @return the Token Revocation endpoint
|
||||
*/
|
||||
public String getTokenRevocationEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.TOKEN_REVOCATION_ENDPOINT);
|
||||
return getSetting(ConfigurationSettingNames.AuthorizationServer.TOKEN_REVOCATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider's OAuth 2.0 Token Introspection endpoint. The default is {@code /oauth2/introspect}.
|
||||
* Returns the OAuth 2.0 Token Introspection endpoint. The default is {@code /oauth2/introspect}.
|
||||
*
|
||||
* @return the Token Introspection endpoint
|
||||
*/
|
||||
public String getTokenIntrospectionEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.TOKEN_INTROSPECTION_ENDPOINT);
|
||||
return getSetting(ConfigurationSettingNames.AuthorizationServer.TOKEN_INTROSPECTION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider's OpenID Connect 1.0 Client Registration endpoint. The default is {@code /connect/register}.
|
||||
* Returns the OpenID Connect 1.0 Client Registration endpoint. The default is {@code /connect/register}.
|
||||
*
|
||||
* @return the OpenID Connect 1.0 Client Registration endpoint
|
||||
*/
|
||||
public String getOidcClientRegistrationEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.OIDC_CLIENT_REGISTRATION_ENDPOINT);
|
||||
return getSetting(ConfigurationSettingNames.AuthorizationServer.OIDC_CLIENT_REGISTRATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Provider's OpenID Connect 1.0 UserInfo endpoint. The default is {@code /userinfo}.
|
||||
* Returns the OpenID Connect 1.0 UserInfo endpoint. The default is {@code /userinfo}.
|
||||
*
|
||||
* @return the OpenID Connect 1.0 UserInfo endpoint
|
||||
*/
|
||||
public String getOidcUserInfoEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.Provider.OIDC_USER_INFO_ENDPOINT);
|
||||
return getSetting(ConfigurationSettingNames.AuthorizationServer.OIDC_USER_INFO_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,101 +135,101 @@ public final class ProviderSettings extends AbstractSettings {
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link ProviderSettings}.
|
||||
* A builder for {@link AuthorizationServerSettings}.
|
||||
*/
|
||||
public final static class Builder extends AbstractBuilder<ProviderSettings, Builder> {
|
||||
public final static class Builder extends AbstractBuilder<AuthorizationServerSettings, Builder> {
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL the Provider uses as its Issuer Identifier.
|
||||
* Sets the URL the Authorization Server uses as its Issuer Identifier.
|
||||
*
|
||||
* @param issuer the URL the Provider uses as its Issuer Identifier.
|
||||
* @param issuer the URL the Authorization Server uses as its Issuer Identifier.
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder issuer(String issuer) {
|
||||
return setting(ConfigurationSettingNames.Provider.ISSUER, issuer);
|
||||
return setting(ConfigurationSettingNames.AuthorizationServer.ISSUER, issuer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OAuth 2.0 Authorization endpoint.
|
||||
* Sets the OAuth 2.0 Authorization endpoint.
|
||||
*
|
||||
* @param authorizationEndpoint the Authorization endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder authorizationEndpoint(String authorizationEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
|
||||
return setting(ConfigurationSettingNames.AuthorizationServer.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OAuth 2.0 Token endpoint.
|
||||
* Sets the OAuth 2.0 Token endpoint.
|
||||
*
|
||||
* @param tokenEndpoint the Token endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder tokenEndpoint(String tokenEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.TOKEN_ENDPOINT, tokenEndpoint);
|
||||
return setting(ConfigurationSettingNames.AuthorizationServer.TOKEN_ENDPOINT, tokenEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's JWK Set endpoint.
|
||||
* Sets the JWK Set endpoint.
|
||||
*
|
||||
* @param jwkSetEndpoint the JWK Set endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder jwkSetEndpoint(String jwkSetEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT, jwkSetEndpoint);
|
||||
return setting(ConfigurationSettingNames.AuthorizationServer.JWK_SET_ENDPOINT, jwkSetEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OAuth 2.0 Token Revocation endpoint.
|
||||
* Sets the OAuth 2.0 Token Revocation endpoint.
|
||||
*
|
||||
* @param tokenRevocationEndpoint the Token Revocation endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder tokenRevocationEndpoint(String tokenRevocationEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.TOKEN_REVOCATION_ENDPOINT, tokenRevocationEndpoint);
|
||||
return setting(ConfigurationSettingNames.AuthorizationServer.TOKEN_REVOCATION_ENDPOINT, tokenRevocationEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OAuth 2.0 Token Introspection endpoint.
|
||||
* Sets the OAuth 2.0 Token Introspection endpoint.
|
||||
*
|
||||
* @param tokenIntrospectionEndpoint the Token Introspection endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder tokenIntrospectionEndpoint(String tokenIntrospectionEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.TOKEN_INTROSPECTION_ENDPOINT, tokenIntrospectionEndpoint);
|
||||
return setting(ConfigurationSettingNames.AuthorizationServer.TOKEN_INTROSPECTION_ENDPOINT, tokenIntrospectionEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OpenID Connect 1.0 Client Registration endpoint.
|
||||
* Sets the OpenID Connect 1.0 Client Registration endpoint.
|
||||
*
|
||||
* @param oidcClientRegistrationEndpoint the OpenID Connect 1.0 Client Registration endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder oidcClientRegistrationEndpoint(String oidcClientRegistrationEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.OIDC_CLIENT_REGISTRATION_ENDPOINT, oidcClientRegistrationEndpoint);
|
||||
return setting(ConfigurationSettingNames.AuthorizationServer.OIDC_CLIENT_REGISTRATION_ENDPOINT, oidcClientRegistrationEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Provider's OpenID Connect 1.0 UserInfo endpoint.
|
||||
* Sets the OpenID Connect 1.0 UserInfo endpoint.
|
||||
*
|
||||
* @param oidcUserInfoEndpoint the OpenID Connect 1.0 UserInfo endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder oidcUserInfoEndpoint(String oidcUserInfoEndpoint) {
|
||||
return setting(ConfigurationSettingNames.Provider.OIDC_USER_INFO_ENDPOINT, oidcUserInfoEndpoint);
|
||||
return setting(ConfigurationSettingNames.AuthorizationServer.OIDC_USER_INFO_ENDPOINT, oidcUserInfoEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link ProviderSettings}.
|
||||
* Builds the {@link AuthorizationServerSettings}.
|
||||
*
|
||||
* @return the {@link ProviderSettings}
|
||||
* @return the {@link AuthorizationServerSettings}
|
||||
*/
|
||||
@Override
|
||||
public ProviderSettings build() {
|
||||
return new ProviderSettings(getSettings());
|
||||
public AuthorizationServerSettings build() {
|
||||
return new AuthorizationServerSettings(getSettings());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -71,52 +71,52 @@ public final class ConfigurationSettingNames {
|
||||
}
|
||||
|
||||
/**
|
||||
* The names for provider configuration settings.
|
||||
* The names for authorization server configuration settings.
|
||||
*/
|
||||
public static final class Provider {
|
||||
private static final String PROVIDER_SETTINGS_NAMESPACE = SETTINGS_NAMESPACE.concat("provider.");
|
||||
public static final class AuthorizationServer {
|
||||
private static final String AUTHORIZATION_SERVER_SETTINGS_NAMESPACE = SETTINGS_NAMESPACE.concat("authorization-server.");
|
||||
|
||||
/**
|
||||
* Set the URL the Provider uses as its Issuer Identifier.
|
||||
* Set the URL the Authorization Server uses as its Issuer Identifier.
|
||||
*/
|
||||
public static final String ISSUER = PROVIDER_SETTINGS_NAMESPACE.concat("issuer");
|
||||
public static final String ISSUER = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("issuer");
|
||||
|
||||
/**
|
||||
* Set the Provider's OAuth 2.0 Authorization endpoint.
|
||||
* Set the OAuth 2.0 Authorization endpoint.
|
||||
*/
|
||||
public static final String AUTHORIZATION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("authorization-endpoint");
|
||||
public static final String AUTHORIZATION_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("authorization-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's OAuth 2.0 Token endpoint.
|
||||
* Set the OAuth 2.0 Token endpoint.
|
||||
*/
|
||||
public static final String TOKEN_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("token-endpoint");
|
||||
public static final String TOKEN_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("token-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's JWK Set endpoint.
|
||||
* Set the JWK Set endpoint.
|
||||
*/
|
||||
public static final String JWK_SET_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("jwk-set-endpoint");
|
||||
public static final String JWK_SET_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("jwk-set-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's OAuth 2.0 Token Revocation endpoint.
|
||||
* Set the OAuth 2.0 Token Revocation endpoint.
|
||||
*/
|
||||
public static final String TOKEN_REVOCATION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("token-revocation-endpoint");
|
||||
public static final String TOKEN_REVOCATION_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("token-revocation-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's OAuth 2.0 Token Introspection endpoint.
|
||||
* Set the OAuth 2.0 Token Introspection endpoint.
|
||||
*/
|
||||
public static final String TOKEN_INTROSPECTION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("token-introspection-endpoint");
|
||||
public static final String TOKEN_INTROSPECTION_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("token-introspection-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's OpenID Connect 1.0 Client Registration endpoint.
|
||||
* Set the OpenID Connect 1.0 Client Registration endpoint.
|
||||
*/
|
||||
public static final String OIDC_CLIENT_REGISTRATION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("oidc-client-registration-endpoint");
|
||||
public static final String OIDC_CLIENT_REGISTRATION_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("oidc-client-registration-endpoint");
|
||||
|
||||
/**
|
||||
* Set the Provider's OpenID Connect 1.0 UserInfo endpoint.
|
||||
* Set the OpenID Connect 1.0 UserInfo endpoint.
|
||||
*/
|
||||
public static final String OIDC_USER_INFO_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("oidc-user-info-endpoint");
|
||||
public static final String OIDC_USER_INFO_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("oidc-user-info-endpoint");
|
||||
|
||||
private Provider() {
|
||||
private AuthorizationServer() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
@@ -82,16 +83,20 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
|
||||
}
|
||||
|
||||
String issuer = null;
|
||||
if (context.getProviderContext() != null) {
|
||||
issuer = context.getProviderContext().getIssuer();
|
||||
if (context.getAuthorizationServerContext() != null) {
|
||||
issuer = context.getAuthorizationServerContext().getIssuer();
|
||||
}
|
||||
RegisteredClient registeredClient = context.getRegisteredClient();
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt;
|
||||
JwsAlgorithm jwsAlgorithm = SignatureAlgorithm.RS256;
|
||||
if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
|
||||
// TODO Allow configuration for ID Token time-to-live
|
||||
expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES);
|
||||
if (registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm() != null) {
|
||||
jwsAlgorithm = registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm();
|
||||
}
|
||||
} else {
|
||||
expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
|
||||
}
|
||||
@@ -125,14 +130,14 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(SignatureAlgorithm.RS256);
|
||||
JwsHeader.Builder jwsHeaderBuilder = JwsHeader.with(jwsAlgorithm);
|
||||
|
||||
if (this.jwtCustomizer != null) {
|
||||
// @formatter:off
|
||||
JwtEncodingContext.Builder jwtContextBuilder = JwtEncodingContext.with(jwsHeaderBuilder, claimsBuilder)
|
||||
.registeredClient(context.getRegisteredClient())
|
||||
.principal(context.getPrincipal())
|
||||
.providerContext(context.getProviderContext())
|
||||
.authorizationServerContext(context.getAuthorizationServerContext())
|
||||
.authorizedScopes(context.getAuthorizedScopes())
|
||||
.tokenType(context.getTokenType())
|
||||
.authorizationGrantType(context.getAuthorizationGrantType());
|
||||
|
||||
@@ -61,8 +61,8 @@ public final class OAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OA
|
||||
}
|
||||
|
||||
String issuer = null;
|
||||
if (context.getProviderContext() != null) {
|
||||
issuer = context.getProviderContext().getIssuer();
|
||||
if (context.getAuthorizationServerContext() != null) {
|
||||
issuer = context.getAuthorizationServerContext().getIssuer();
|
||||
}
|
||||
RegisteredClient registeredClient = context.getRegisteredClient();
|
||||
|
||||
@@ -91,7 +91,7 @@ public final class OAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OA
|
||||
OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder)
|
||||
.registeredClient(context.getRegisteredClient())
|
||||
.principal(context.getPrincipal())
|
||||
.providerContext(context.getProviderContext())
|
||||
.authorizationServerContext(context.getAuthorizationServerContext())
|
||||
.authorizedScopes(context.getAuthorizedScopes())
|
||||
.tokenType(context.getTokenType())
|
||||
.authorizationGrantType(context.getAuthorizationGrantType());
|
||||
|
||||
@@ -27,8 +27,8 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
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.Context;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -63,13 +63,13 @@ public interface OAuth2TokenContext extends Context {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ProviderContext provider context}.
|
||||
* Returns the {@link AuthorizationServerContext authorization server context}.
|
||||
*
|
||||
* @return the {@link ProviderContext}
|
||||
* @return the {@link AuthorizationServerContext}
|
||||
* @since 0.2.3
|
||||
*/
|
||||
default ProviderContext getProviderContext() {
|
||||
return get(ProviderContext.class);
|
||||
default AuthorizationServerContext getAuthorizationServerContext() {
|
||||
return get(AuthorizationServerContext.class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,14 +157,14 @@ public interface OAuth2TokenContext extends Context {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ProviderContext provider context}.
|
||||
* Sets the {@link AuthorizationServerContext authorization server context}.
|
||||
*
|
||||
* @param providerContext the {@link ProviderContext}
|
||||
* @param authorizationServerContext the {@link AuthorizationServerContext}
|
||||
* @return the {@link AbstractBuilder} for further configuration
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public B providerContext(ProviderContext providerContext) {
|
||||
return put(ProviderContext.class, providerContext);
|
||||
public B authorizationServerContext(AuthorizationServerContext authorizationServerContext) {
|
||||
return put(AuthorizationServerContext.class, authorizationServerContext);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,7 +24,7 @@ package org.springframework.security.oauth2.server.authorization.util;
|
||||
public final class SpringAuthorizationServerVersion {
|
||||
private static final int MAJOR = 0;
|
||||
private static final int MINOR = 4;
|
||||
private static final int PATCH = 0;
|
||||
private static final int PATCH = 1;
|
||||
|
||||
/**
|
||||
* Global Serialization value for Spring Authorization Server classes.
|
||||
|
||||
@@ -17,7 +17,10 @@ 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;
|
||||
@@ -25,9 +28,11 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -40,7 +45,11 @@ import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
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.OAuth2AuthorizationConsentAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
@@ -61,7 +70,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
|
||||
* which handles the processing of the OAuth 2.0 Authorization Request (and Consent).
|
||||
* which handles the processing of the OAuth 2.0 Authorization Request and Consent.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Paurav Munshi
|
||||
@@ -71,6 +80,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
* @since 0.0.1
|
||||
* @see AuthenticationManager
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
|
||||
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
|
||||
@@ -110,7 +120,10 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
Assert.hasText(authorizationEndpointUri, "authorizationEndpointUri cannot be empty");
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.authorizationEndpointMatcher = createDefaultRequestMatcher(authorizationEndpointUri);
|
||||
this.authenticationConverter = new OAuth2AuthorizationCodeRequestAuthenticationConverter();
|
||||
this.authenticationConverter = new DelegatingAuthenticationConverter(
|
||||
Arrays.asList(
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationConverter(),
|
||||
new OAuth2AuthorizationConsentAuthenticationConverter()));
|
||||
}
|
||||
|
||||
private static RequestMatcher createDefaultRequestMatcher(String authorizationEndpointUri) {
|
||||
@@ -145,14 +158,14 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
}
|
||||
|
||||
try {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationConverter.convert(request);
|
||||
authorizationCodeRequestAuthentication.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||
Authentication authentication = this.authenticationConverter.convert(request);
|
||||
if (authentication instanceof AbstractAuthenticationToken) {
|
||||
((AbstractAuthenticationToken) authentication)
|
||||
.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||
}
|
||||
Authentication authenticationResult = this.authenticationManager.authenticate(authentication);
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationManager.authenticate(authorizationCodeRequestAuthentication);
|
||||
|
||||
if (!authorizationCodeRequestAuthenticationResult.isAuthenticated()) {
|
||||
if (!authenticationResult.isAuthenticated()) {
|
||||
// If the Principal (Resource Owner) is not authenticated then
|
||||
// pass through the chain with the expectation that the authentication process
|
||||
// will commence via AuthenticationEntryPoint
|
||||
@@ -160,15 +173,23 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
return;
|
||||
}
|
||||
|
||||
if (authorizationCodeRequestAuthenticationResult.isConsentRequired()) {
|
||||
sendAuthorizationConsent(request, response, authorizationCodeRequestAuthentication, authorizationCodeRequestAuthenticationResult);
|
||||
if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Authorization consent is required");
|
||||
}
|
||||
sendAuthorizationConsent(request, response,
|
||||
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication,
|
||||
(OAuth2AuthorizationConsentAuthenticationToken) authenticationResult);
|
||||
return;
|
||||
}
|
||||
|
||||
this.authenticationSuccessHandler.onAuthenticationSuccess(
|
||||
request, response, authorizationCodeRequestAuthenticationResult);
|
||||
request, response, authenticationResult);
|
||||
|
||||
} catch (OAuth2AuthenticationException ex) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Authorization request failed: %s", ex.getError()), ex);
|
||||
}
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
|
||||
}
|
||||
}
|
||||
@@ -186,7 +207,8 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request.
|
||||
* to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} or {@link OAuth2AuthorizationConsentAuthenticationToken}
|
||||
* used for authenticating the request.
|
||||
*
|
||||
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
*/
|
||||
@@ -229,13 +251,13 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
|
||||
private void sendAuthorizationConsent(HttpServletRequest request, HttpServletResponse response,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult) throws IOException {
|
||||
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication) throws IOException {
|
||||
|
||||
String clientId = authorizationCodeRequestAuthenticationResult.getClientId();
|
||||
Authentication principal = (Authentication) authorizationCodeRequestAuthenticationResult.getPrincipal();
|
||||
String clientId = authorizationConsentAuthentication.getClientId();
|
||||
Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal();
|
||||
Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
|
||||
Set<String> authorizedScopes = authorizationCodeRequestAuthenticationResult.getScopes();
|
||||
String state = authorizationCodeRequestAuthenticationResult.getState();
|
||||
Set<String> authorizedScopes = authorizationConsentAuthentication.getScopes();
|
||||
String state = authorizationConsentAuthentication.getState();
|
||||
|
||||
if (hasConsentUri()) {
|
||||
String redirectUri = UriComponentsBuilder.fromUriString(resolveConsentUri(request))
|
||||
@@ -245,6 +267,9 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
.toUriString();
|
||||
this.redirectStrategy.sendRedirect(request, response, redirectUri);
|
||||
} else {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Displaying generated consent screen");
|
||||
}
|
||||
DefaultConsentPage.displayConsent(request, response, clientId, principal, requestedScopes, authorizedScopes, state);
|
||||
}
|
||||
}
|
||||
@@ -274,10 +299,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,
|
||||
@@ -295,6 +326,10 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Redirecting to client with error");
|
||||
}
|
||||
|
||||
UriComponentsBuilder uriBuilder = UriComponentsBuilder
|
||||
.fromUriString(authorizationCodeRequestAuthentication.getRedirectUri())
|
||||
.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
|
||||
@@ -304,10 +339,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,9 +31,10 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
|
||||
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.http.converter.OAuth2AuthorizationServerMetadataHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -44,9 +45,10 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
* A {@code Filter} that processes OAuth 2.0 Authorization Server Metadata Requests.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.1
|
||||
* @see OAuth2AuthorizationServerMetadata
|
||||
* @see ProviderSettings
|
||||
* @see AuthorizationServerSettings
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3">3. Obtaining Authorization Server Metadata</a>
|
||||
*/
|
||||
public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OncePerRequestFilter {
|
||||
@@ -55,18 +57,23 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
|
||||
*/
|
||||
private static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server";
|
||||
|
||||
private final ProviderSettings providerSettings;
|
||||
private final RequestMatcher requestMatcher;
|
||||
private final RequestMatcher requestMatcher = new AntPathRequestMatcher(
|
||||
DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI,
|
||||
HttpMethod.GET.name());
|
||||
private final OAuth2AuthorizationServerMetadataHttpMessageConverter authorizationServerMetadataHttpMessageConverter =
|
||||
new OAuth2AuthorizationServerMetadataHttpMessageConverter();
|
||||
private Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer = (authorizationServerMetadata) -> {};
|
||||
|
||||
public OAuth2AuthorizationServerMetadataEndpointFilter(ProviderSettings providerSettings) {
|
||||
Assert.notNull(providerSettings, "providerSettings cannot be null");
|
||||
this.providerSettings = providerSettings;
|
||||
this.requestMatcher = new AntPathRequestMatcher(
|
||||
DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI,
|
||||
HttpMethod.GET.name()
|
||||
);
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder}
|
||||
* allowing the ability to customize the claims of the Authorization Server's configuration.
|
||||
*
|
||||
* @param authorizationServerMetadataCustomizer the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder}
|
||||
* @since 0.4.0
|
||||
*/
|
||||
public void setAuthorizationServerMetadataCustomizer(Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer) {
|
||||
Assert.notNull(authorizationServerMetadataCustomizer, "authorizationServerMetadataCustomizer cannot be null");
|
||||
this.authorizationServerMetadataCustomizer = authorizationServerMetadataCustomizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -78,28 +85,31 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
|
||||
return;
|
||||
}
|
||||
|
||||
String issuer = ProviderContextHolder.getProviderContext().getIssuer();
|
||||
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
|
||||
String issuer = authorizationServerContext.getIssuer();
|
||||
AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings();
|
||||
|
||||
OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder()
|
||||
OAuth2AuthorizationServerMetadata.Builder authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder()
|
||||
.issuer(issuer)
|
||||
.authorizationEndpoint(asUrl(issuer, this.providerSettings.getAuthorizationEndpoint()))
|
||||
.tokenEndpoint(asUrl(issuer, this.providerSettings.getTokenEndpoint()))
|
||||
.authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint()))
|
||||
.tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint()))
|
||||
.tokenEndpointAuthenticationMethods(clientAuthenticationMethods())
|
||||
.jwkSetUrl(asUrl(issuer, this.providerSettings.getJwkSetEndpoint()))
|
||||
.jwkSetUrl(asUrl(issuer, authorizationServerSettings.getJwkSetEndpoint()))
|
||||
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
|
||||
.tokenRevocationEndpoint(asUrl(issuer, this.providerSettings.getTokenRevocationEndpoint()))
|
||||
.tokenRevocationEndpoint(asUrl(issuer, authorizationServerSettings.getTokenRevocationEndpoint()))
|
||||
.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
|
||||
.tokenIntrospectionEndpoint(asUrl(issuer, this.providerSettings.getTokenIntrospectionEndpoint()))
|
||||
.tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint()))
|
||||
.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
|
||||
.codeChallengeMethod("S256")
|
||||
.build();
|
||||
.codeChallengeMethod("S256");
|
||||
|
||||
this.authorizationServerMetadataCustomizer.accept(authorizationServerMetadata);
|
||||
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
this.authorizationServerMetadataHttpMessageConverter.write(
|
||||
authorizationServerMetadata, MediaType.APPLICATION_JSON, httpResponse);
|
||||
authorizationServerMetadata.build(), MediaType.APPLICATION_JSON, httpResponse);
|
||||
}
|
||||
|
||||
private static Consumer<List<String>> clientAuthenticationMethods() {
|
||||
|
||||
@@ -23,6 +23,7 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
@@ -117,12 +118,16 @@ public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter
|
||||
this.authenticationDetailsSource.buildDetails(request));
|
||||
}
|
||||
if (authenticationRequest != null) {
|
||||
validateClientIdentifier(authenticationRequest);
|
||||
Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
|
||||
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
} catch (OAuth2AuthenticationException ex) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Client authentication failed: %s", ex.getError()), ex);
|
||||
}
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
|
||||
}
|
||||
}
|
||||
@@ -166,6 +171,10 @@ public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
securityContext.setAuthentication(authentication);
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug(LogMessage.format("Set SecurityContextHolder authentication to %s",
|
||||
authentication.getClass().getSimpleName()));
|
||||
}
|
||||
}
|
||||
|
||||
private void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
|
||||
@@ -193,4 +202,25 @@ public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter
|
||||
this.errorHttpResponseConverter.write(errorResponse, null, httpResponse);
|
||||
}
|
||||
|
||||
private static void validateClientIdentifier(Authentication authentication) {
|
||||
if (!(authentication instanceof OAuth2ClientAuthenticationToken)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// As per spec, in Appendix A.1.
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#appendix-A.1
|
||||
// The syntax for client_id is *VSCHAR (%x20-7E):
|
||||
// -> Hex 20 -> ASCII 32 -> space
|
||||
// -> Hex 7E -> ASCII 126 -> tilde
|
||||
|
||||
OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication;
|
||||
String clientId = (String) clientAuthentication.getPrincipal();
|
||||
for (int i = 0; i < clientId.length(); i++) {
|
||||
char charAt = clientId.charAt(i);
|
||||
if (!(charAt >= 32 && charAt <= 126)) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
@@ -167,6 +168,9 @@ public final class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
||||
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, accessTokenAuthentication);
|
||||
} catch (OAuth2AuthenticationException ex) {
|
||||
SecurityContextHolder.clearContext();
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Token request failed: %s", ex.getError()), ex);
|
||||
}
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,13 @@
|
||||
package org.springframework.security.oauth2.server.authorization.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
@@ -34,21 +33,18 @@ 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.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenIntrospection;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.http.converter.OAuth2TokenIntrospectionHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenIntrospectionAuthenticationConverter;
|
||||
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.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
@@ -70,8 +66,7 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final RequestMatcher tokenIntrospectionEndpointMatcher;
|
||||
private AuthenticationConverter authenticationConverter =
|
||||
new DefaultTokenIntrospectionAuthenticationConverter();
|
||||
private AuthenticationConverter authenticationConverter;
|
||||
private final HttpMessageConverter<OAuth2TokenIntrospection> tokenIntrospectionHttpResponseConverter =
|
||||
new OAuth2TokenIntrospectionHttpMessageConverter();
|
||||
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
|
||||
@@ -100,6 +95,7 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.tokenIntrospectionEndpointMatcher = new AntPathRequestMatcher(
|
||||
tokenIntrospectionEndpointUri, HttpMethod.POST.name());
|
||||
this.authenticationConverter = new OAuth2TokenIntrospectionAuthenticationConverter();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -118,6 +114,9 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest
|
||||
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, tokenIntrospectionAuthenticationResult);
|
||||
} catch (OAuth2AuthenticationException ex) {
|
||||
SecurityContextHolder.clearContext();
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Token introspection request failed: %s", ex.getError()), ex);
|
||||
}
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
|
||||
}
|
||||
}
|
||||
@@ -175,47 +174,4 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest
|
||||
this.errorHttpResponseConverter.write(error, null, httpResponse);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Token Introspection Parameter: " + parameterName,
|
||||
"https://datatracker.ietf.org/doc/html/rfc7662#section-2.1");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
private static class DefaultTokenIntrospectionAuthenticationConverter
|
||||
implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
// token (REQUIRED)
|
||||
String token = parameters.getFirst(OAuth2ParameterNames.TOKEN);
|
||||
if (!StringUtils.hasText(token) ||
|
||||
parameters.get(OAuth2ParameterNames.TOKEN).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN);
|
||||
}
|
||||
|
||||
// token_type_hint (OPTIONAL)
|
||||
String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames.TOKEN_TYPE_HINT);
|
||||
if (StringUtils.hasText(tokenTypeHint) &&
|
||||
parameters.get(OAuth2ParameterNames.TOKEN_TYPE_HINT).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN_TYPE_HINT);
|
||||
}
|
||||
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
parameters.forEach((key, value) -> {
|
||||
if (!key.equals(OAuth2ParameterNames.TOKEN) &&
|
||||
!key.equals(OAuth2ParameterNames.TOKEN_TYPE_HINT)) {
|
||||
additionalParameters.put(key, value.get(0));
|
||||
}
|
||||
});
|
||||
|
||||
return new OAuth2TokenIntrospectionAuthenticationToken(
|
||||
token, clientPrincipal, tokenTypeHint, additionalParameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
@@ -32,19 +33,16 @@ 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.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationAuthenticationConverter;
|
||||
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.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
@@ -66,8 +64,7 @@ public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFil
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final RequestMatcher tokenRevocationEndpointMatcher;
|
||||
private AuthenticationConverter authenticationConverter =
|
||||
new DefaultTokenRevocationAuthenticationConverter();
|
||||
private AuthenticationConverter authenticationConverter;
|
||||
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
|
||||
new OAuth2ErrorHttpMessageConverter();
|
||||
private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendRevocationSuccessResponse;
|
||||
@@ -95,6 +92,7 @@ public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFil
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.tokenRevocationEndpointMatcher = new AntPathRequestMatcher(
|
||||
tokenRevocationEndpointUri, HttpMethod.POST.name());
|
||||
this.authenticationConverter = new OAuth2TokenRevocationAuthenticationConverter();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -113,15 +111,18 @@ public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFil
|
||||
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, tokenRevocationAuthenticationResult);
|
||||
} catch (OAuth2AuthenticationException ex) {
|
||||
SecurityContextHolder.clearContext();
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Token revocation request failed: %s", ex.getError()), ex);
|
||||
}
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract a Revoke Token Request from {@link HttpServletRequest}
|
||||
* to an instance of {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the client.
|
||||
* to an instance of {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
|
||||
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract a Revoke Token Request from {@link HttpServletRequest}
|
||||
* @since 0.2.2
|
||||
*/
|
||||
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
|
||||
@@ -164,36 +165,4 @@ public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFil
|
||||
this.errorHttpResponseConverter.write(error, null, httpResponse);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Token Revocation Parameter: " + parameterName,
|
||||
"https://datatracker.ietf.org/doc/html/rfc7009#section-2.1");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
private static class DefaultTokenRevocationAuthenticationConverter
|
||||
implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
// token (REQUIRED)
|
||||
String token = parameters.getFirst(OAuth2ParameterNames.TOKEN);
|
||||
if (!StringUtils.hasText(token) ||
|
||||
parameters.get(OAuth2ParameterNames.TOKEN).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN);
|
||||
}
|
||||
|
||||
// token_type_hint (OPTIONAL)
|
||||
String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames.TOKEN_TYPE_HINT);
|
||||
if (StringUtils.hasText(tokenTypeHint) &&
|
||||
parameters.get(OAuth2ParameterNames.TOKEN_TYPE_HINT).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN_TYPE_HINT);
|
||||
}
|
||||
|
||||
return new OAuth2TokenRevocationAuthenticationToken(token, clientPrincipal, tokenTypeHint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* 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.web;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A {@code Filter} that associates the {@link ProviderContext} to the {@link ProviderContextHolder}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.2.2
|
||||
* @see ProviderContext
|
||||
* @see ProviderContextHolder
|
||||
* @see ProviderSettings
|
||||
*/
|
||||
public final class ProviderContextFilter extends OncePerRequestFilter {
|
||||
private final ProviderSettings providerSettings;
|
||||
|
||||
/**
|
||||
* Constructs a {@code ProviderContextFilter} using the provided parameters.
|
||||
*
|
||||
* @param providerSettings the provider settings
|
||||
*/
|
||||
public ProviderContextFilter(ProviderSettings providerSettings) {
|
||||
Assert.notNull(providerSettings, "providerSettings cannot be null");
|
||||
this.providerSettings = providerSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
try {
|
||||
ProviderContext providerContext = new ProviderContext(
|
||||
this.providerSettings, () -> resolveIssuer(this.providerSettings, request));
|
||||
ProviderContextHolder.setProviderContext(providerContext);
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
ProviderContextHolder.resetProviderContext();
|
||||
}
|
||||
}
|
||||
|
||||
private static String resolveIssuer(ProviderSettings providerSettings, HttpServletRequest request) {
|
||||
return providerSettings.getIssuer() != null ?
|
||||
providerSettings.getIssuer() :
|
||||
getContextPath(request);
|
||||
}
|
||||
|
||||
private static String getContextPath(HttpServletRequest request) {
|
||||
// @formatter:off
|
||||
return UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
||||
.replacePath(request.getContextPath())
|
||||
.replaceQuery(null)
|
||||
.fragment(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
@@ -43,7 +43,7 @@ import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Attempts to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
|
||||
* Attempts to extract an Authorization Request from {@link HttpServletRequest}
|
||||
* for the OAuth 2.0 Authorization Code Grant and then converts it to
|
||||
* an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
@@ -62,20 +62,19 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
if (!"GET".equals(request.getMethod()) && !OIDC_REQUEST_MATCHER.matches(request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
boolean authorizationRequest = false;
|
||||
if ("GET".equals(request.getMethod()) || OIDC_REQUEST_MATCHER.matches(request)) {
|
||||
authorizationRequest = true;
|
||||
|
||||
// response_type (REQUIRED)
|
||||
String responseType = request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
if (!StringUtils.hasText(responseType) ||
|
||||
parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
} else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
|
||||
throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
}
|
||||
// response_type (REQUIRED)
|
||||
String responseType = request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
if (!StringUtils.hasText(responseType) ||
|
||||
parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
} else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
|
||||
throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
}
|
||||
|
||||
String authorizationUri = request.getRequestURL().toString();
|
||||
@@ -101,37 +100,21 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
|
||||
|
||||
// scope (OPTIONAL)
|
||||
Set<String> scopes = null;
|
||||
if (authorizationRequest) {
|
||||
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
|
||||
if (StringUtils.hasText(scope) &&
|
||||
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
|
||||
}
|
||||
if (StringUtils.hasText(scope)) {
|
||||
scopes = new HashSet<>(
|
||||
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
|
||||
}
|
||||
} else {
|
||||
// Consent request
|
||||
if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) {
|
||||
scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE));
|
||||
}
|
||||
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
|
||||
if (StringUtils.hasText(scope) &&
|
||||
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
|
||||
}
|
||||
if (StringUtils.hasText(scope)) {
|
||||
scopes = new HashSet<>(
|
||||
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
|
||||
}
|
||||
|
||||
// state
|
||||
// RECOMMENDED for Authorization Request
|
||||
// state (RECOMMENDED)
|
||||
String state = parameters.getFirst(OAuth2ParameterNames.STATE);
|
||||
if (authorizationRequest) {
|
||||
if (StringUtils.hasText(state) &&
|
||||
parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
|
||||
}
|
||||
} else {
|
||||
// REQUIRED for Authorization Consent Request
|
||||
if (!StringUtils.hasText(state) ||
|
||||
parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
|
||||
}
|
||||
if (StringUtils.hasText(state) &&
|
||||
parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
|
||||
}
|
||||
|
||||
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
|
||||
@@ -159,14 +142,8 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
|
||||
}
|
||||
});
|
||||
|
||||
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, principal)
|
||||
.authorizationUri(authorizationUri)
|
||||
.redirectUri(redirectUri)
|
||||
.scopes(scopes)
|
||||
.state(state)
|
||||
.additionalParameters(additionalParameters)
|
||||
.consent(!authorizationRequest)
|
||||
.build();
|
||||
return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal,
|
||||
redirectUri, state, scopes, additionalParameters);
|
||||
}
|
||||
|
||||
private static RequestMatcher createOidcRequestMatcher() {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.web.authentication;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
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.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Attempts to extract an Authorization Consent from {@link HttpServletRequest}
|
||||
* for the OAuth 2.0 Authorization Code Grant and then converts it to
|
||||
* an {@link OAuth2AuthorizationConsentAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see AuthenticationConverter
|
||||
* @see OAuth2AuthorizationConsentAuthenticationToken
|
||||
* @see OAuth2AuthorizationEndpointFilter
|
||||
*/
|
||||
public final class OAuth2AuthorizationConsentAuthenticationConverter implements AuthenticationConverter {
|
||||
private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
|
||||
private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken(
|
||||
"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
if (!"POST".equals(request.getMethod()) ||
|
||||
request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
String authorizationUri = request.getRequestURL().toString();
|
||||
|
||||
// client_id (REQUIRED)
|
||||
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
|
||||
if (!StringUtils.hasText(clientId) ||
|
||||
parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
|
||||
}
|
||||
|
||||
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (principal == null) {
|
||||
principal = ANONYMOUS_AUTHENTICATION;
|
||||
}
|
||||
|
||||
// state (REQUIRED)
|
||||
String state = parameters.getFirst(OAuth2ParameterNames.STATE);
|
||||
if (!StringUtils.hasText(state) ||
|
||||
parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
|
||||
}
|
||||
|
||||
// scope (OPTIONAL)
|
||||
Set<String> scopes = null;
|
||||
if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) {
|
||||
scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE));
|
||||
}
|
||||
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
parameters.forEach((key, value) -> {
|
||||
if (!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||
!key.equals(OAuth2ParameterNames.STATE) &&
|
||||
!key.equals(OAuth2ParameterNames.SCOPE)) {
|
||||
additionalParameters.put(key, value.get(0));
|
||||
}
|
||||
});
|
||||
|
||||
return new OAuth2AuthorizationConsentAuthenticationToken(authorizationUri, clientId, principal,
|
||||
state, scopes, additionalParameters);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, DEFAULT_ERROR_URI);
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.web.authentication;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
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.authentication.OAuth2TokenIntrospectionAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Attempts to extract an Introspection Request from {@link HttpServletRequest}
|
||||
* and then converts it to an {@link OAuth2TokenIntrospectionAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @author Gerardo Roza
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see AuthenticationConverter
|
||||
* @see OAuth2TokenIntrospectionAuthenticationToken
|
||||
* @see OAuth2TokenIntrospectionEndpointFilter
|
||||
*/
|
||||
public final class OAuth2TokenIntrospectionAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
// token (REQUIRED)
|
||||
String token = parameters.getFirst(OAuth2ParameterNames.TOKEN);
|
||||
if (!StringUtils.hasText(token) ||
|
||||
parameters.get(OAuth2ParameterNames.TOKEN).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN);
|
||||
}
|
||||
|
||||
// token_type_hint (OPTIONAL)
|
||||
String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames.TOKEN_TYPE_HINT);
|
||||
if (StringUtils.hasText(tokenTypeHint) &&
|
||||
parameters.get(OAuth2ParameterNames.TOKEN_TYPE_HINT).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN_TYPE_HINT);
|
||||
}
|
||||
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
parameters.forEach((key, value) -> {
|
||||
if (!key.equals(OAuth2ParameterNames.TOKEN) &&
|
||||
!key.equals(OAuth2ParameterNames.TOKEN_TYPE_HINT)) {
|
||||
additionalParameters.put(key, value.get(0));
|
||||
}
|
||||
});
|
||||
|
||||
return new OAuth2TokenIntrospectionAuthenticationToken(
|
||||
token, clientPrincipal, tokenTypeHint, additionalParameters);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Token Introspection Parameter: " + parameterName,
|
||||
"https://datatracker.ietf.org/doc/html/rfc7662#section-2.1");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.web.authentication;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
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.authentication.OAuth2TokenRevocationAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Attempts to extract a Revoke Token Request from {@link HttpServletRequest}
|
||||
* and then converts it to an {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the request.
|
||||
*
|
||||
* @author Vivek Babu
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @see AuthenticationConverter
|
||||
* @see OAuth2TokenRevocationAuthenticationToken
|
||||
* @see OAuth2TokenRevocationEndpointFilter
|
||||
*/
|
||||
public final class OAuth2TokenRevocationAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
|
||||
// token (REQUIRED)
|
||||
String token = parameters.getFirst(OAuth2ParameterNames.TOKEN);
|
||||
if (!StringUtils.hasText(token) ||
|
||||
parameters.get(OAuth2ParameterNames.TOKEN).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN);
|
||||
}
|
||||
|
||||
// token_type_hint (OPTIONAL)
|
||||
String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames.TOKEN_TYPE_HINT);
|
||||
if (StringUtils.hasText(tokenTypeHint) &&
|
||||
parameters.get(OAuth2ParameterNames.TOKEN_TYPE_HINT).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN_TYPE_HINT);
|
||||
}
|
||||
|
||||
return new OAuth2TokenRevocationAuthenticationToken(token, clientPrincipal, tokenTypeHint);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Token Revocation Parameter: " + parameterName,
|
||||
"https://datatracker.ietf.org/doc/html/rfc7009#section-2.1");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
@@ -17,8 +17,8 @@ package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
@@ -40,7 +40,7 @@ public class InMemoryOAuth2AuthorizationConsentServiceTests {
|
||||
|
||||
private InMemoryOAuth2AuthorizationConsentService authorizationConsentService;
|
||||
|
||||
@Before
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService();
|
||||
this.authorizationConsentService.save(AUTHORIZATION_CONSENT);
|
||||
|
||||
@@ -19,8 +19,8 @@ import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
@@ -49,7 +49,7 @@ public class InMemoryOAuth2AuthorizationServiceTests {
|
||||
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
|
||||
private InMemoryOAuth2AuthorizationService authorizationService;
|
||||
|
||||
@Before
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.authorizationService = new InMemoryOAuth2AuthorizationService();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -20,9 +20,9 @@ import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
|
||||
@@ -75,7 +75,7 @@ public class JdbcOAuth2AuthorizationConsentServiceTests {
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
private JdbcOAuth2AuthorizationConsentService authorizationConsentService;
|
||||
|
||||
@Before
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.db = createDb();
|
||||
this.jdbcOperations = new JdbcTemplate(this.db);
|
||||
@@ -83,7 +83,7 @@ public class JdbcOAuth2AuthorizationConsentServiceTests {
|
||||
this.authorizationConsentService = new JdbcOAuth2AuthorizationConsentService(this.jdbcOperations, this.registeredClientRepository);
|
||||
}
|
||||
|
||||
@After
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
this.db.shutdown();
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
|
||||
@@ -88,7 +88,7 @@ public class JdbcOAuth2AuthorizationServiceTests {
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
private JdbcOAuth2AuthorizationService authorizationService;
|
||||
|
||||
@Before
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.db = createDb();
|
||||
this.jdbcOperations = new JdbcTemplate(this.db);
|
||||
@@ -96,7 +96,7 @@ public class JdbcOAuth2AuthorizationServiceTests {
|
||||
this.authorizationService = new JdbcOAuth2AuthorizationService(this.jdbcOperations, this.registeredClientRepository);
|
||||
}
|
||||
|
||||
@After
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
this.db.shutdown();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata.Builder;
|
||||
|
||||
@@ -18,7 +18,7 @@ package org.springframework.security.oauth2.server.authorization;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
@@ -67,7 +69,7 @@ public class ClientSecretAuthenticationProviderTests {
|
||||
private ClientSecretAuthenticationProvider authenticationProvider;
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Before
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
@@ -182,6 +184,26 @@ public class ClientSecretAuthenticationProviderTests {
|
||||
verify(this.passwordEncoder).matches(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenExpiredClientSecretThenThrowOAuth2AuthenticationException() {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientSecretExpiresAt(Instant.now().minus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.SECONDS))
|
||||
.build();
|
||||
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
|
||||
registeredClient.getClientId(), ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret(), 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()).contains("client_secret_expires_at");
|
||||
});
|
||||
verify(this.passwordEncoder).matches(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidCredentialsThenAuthenticated() {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
|
||||
@@ -29,8 +29,8 @@ import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.OctetSequenceKey;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
@@ -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;
|
||||
@@ -57,10 +56,10 @@ import org.springframework.security.oauth2.server.authorization.TestOAuth2Author
|
||||
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.ProviderContext;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
|
||||
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.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -91,16 +90,16 @@ public class JwtClientAssertionAuthenticationProviderTests {
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
private JwtClientAssertionAuthenticationProvider authenticationProvider;
|
||||
private ProviderSettings providerSettings;
|
||||
private AuthorizationServerSettings authorizationServerSettings;
|
||||
|
||||
@Before
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
this.authenticationProvider = new JwtClientAssertionAuthenticationProvider(
|
||||
this.registeredClientRepository, this.authorizationService);
|
||||
this.providerSettings = ProviderSettings.builder().issuer("https://auth-server.com").build();
|
||||
ProviderContextHolder.setProviderContext(new ProviderContext(this.providerSettings, null));
|
||||
this.authorizationServerSettings = AuthorizationServerSettings.builder().issuer("https://auth-server.com").build();
|
||||
AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(this.authorizationServerSettings, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -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
|
||||
@@ -421,7 +349,7 @@ public class JwtClientAssertionAuthenticationProviderTests {
|
||||
return JwtClaimsSet.builder()
|
||||
.issuer(registeredClient.getClientId())
|
||||
.subject(registeredClient.getClientId())
|
||||
.audience(Collections.singletonList(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getTokenEndpoint())))
|
||||
.audience(Collections.singletonList(asUrl(this.authorizationServerSettings.getIssuer(), this.authorizationServerSettings.getTokenEndpoint())))
|
||||
.issuedAt(issuedAt)
|
||||
.expiresAt(expiresAt);
|
||||
}
|
||||
|
||||
@@ -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.jupiter.api.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'.");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
|
||||
@@ -24,9 +24,9 @@ import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
@@ -54,10 +54,10 @@ 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.TestRegisteredClients;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
|
||||
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
|
||||
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.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
||||
@@ -97,7 +97,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||
private OAuth2TokenGenerator<?> tokenGenerator;
|
||||
private OAuth2AuthorizationCodeAuthenticationProvider authenticationProvider;
|
||||
|
||||
@Before
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
this.jwtEncoder = mock(JwtEncoder.class);
|
||||
@@ -118,13 +118,13 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||
});
|
||||
this.authenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(
|
||||
this.authorizationService, this.tokenGenerator);
|
||||
ProviderSettings providerSettings = ProviderSettings.builder().issuer("https://provider.com").build();
|
||||
ProviderContextHolder.setProviderContext(new ProviderContext(providerSettings, null));
|
||||
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().issuer("https://provider.com").build();
|
||||
AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
|
||||
}
|
||||
|
||||
@After
|
||||
@AfterEach
|
||||
public void cleanup() {
|
||||
ProviderContextHolder.resetProviderContext();
|
||||
AuthorizationServerContextHolder.resetContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user