109 Commits
0.1.2 ... 0.2.1

Author SHA1 Message Date
Joe Grandja
28ac43bd50 Release 0.2.1 2021-12-01 08:23:54 -05:00
Joe Grandja
5e684fedbe Authorization Consent request state parameter is validated
Closes gh-503
2021-12-01 07:55:17 -05:00
Joe Grandja
086a3b01a1 Update to jackson-bom 2.12.5
Closes gh-517
2021-11-30 15:38:36 -05:00
Joe Grandja
d10fe36ec0 Update to Spring Boot 2.5.7
Closes gh-516
2021-11-30 15:38:14 -05:00
Joe Grandja
cd2a990df5 Update Reactor to 2020.0.13
Closes gh-515
2021-11-30 15:37:55 -05:00
Joe Grandja
dc77bcdd5a Update to Spring Security 5.5.3
Closes gh-514
2021-11-30 15:37:43 -05:00
Joe Grandja
c0ac18d53a Update to Spring Framework 5.3.13
Closes gh-513
2021-11-30 15:36:40 -05:00
Joe Grandja
14cb58df2b Increase size for attributes column
Issue gh-304
2021-11-30 13:33:48 -05:00
Ovidiu Popa
2e2c9ea286 Fix registration access token cannot be deserialized
Change the authorized scopes Set from SingletonSet to UnmodifiableSet as there is no mixin registered for SingletonSet

Closes gh-495
2021-11-30 13:17:06 -05:00
Joe Grandja
82e4f3a345 Introduce OidcUserInfoAuthenticationContext
Issue gh-441
2021-11-30 11:56:08 -05:00
Joe Grandja
9b60ed23e1 Polish OAuth2AuthorizationConsentAuthenticationContext
Issue gh-470
2021-11-30 06:16:01 -05:00
Joe Grandja
332d1cc318 Polish gh-492 2021-11-30 05:27:32 -05:00
Joe Grandja
8defe2eb3a ProviderSettings @Bean is required
Issue gh-373
2021-11-29 02:22:21 -05:00
Joe Grandja
830f55e538 Revert "Support resolving issuer from current request"
This reverts commit 666d569b48.
2021-11-29 01:49:26 -05:00
Joe Grandja
c418306fd9 Polish Authorization Consent Deny Request
Issue gh-470
2021-11-26 06:46:05 -05:00
Joe Grandja
9053e3188d Fix broken documentation links in README
Closes gh-494
2021-11-17 05:31:46 -05:00
Joe Grandja
f0b19f30d1 Deprecate PasswordEncoder in JdbcRegisteredClientRepository
Closes gh-496
2021-11-16 08:54:50 -05:00
Joe Grandja
666d569b48 Support resolving issuer from current request
Closes gh-479
2021-11-15 15:17:51 -05:00
Joe Grandja
646ea00db2 Add @since
Issue gh-427
2021-11-15 11:26:54 -05:00
Joe Grandja
d4357197c9 Polish gh-470 2021-11-12 11:54:59 -05:00
Steve Riesenberg
4ce999c014 Customize OAuth2AuthorizationConsent prior to saving
Closes gh-436
2021-11-12 11:54:59 -05:00
Joe Grandja
5fa1e8e3b1 Allow subclassing OAuth2AuthenticationContext
Closes gh-492
2021-11-12 11:44:57 -05:00
Joe Grandja
5982d2285c Restructure samples
Closes gh-485
2021-11-09 06:01:51 -05:00
Joe Grandja
b455268fa1 Polish OAuth2ClientAuthenticationProviderTests 2021-11-03 12:31:38 -04:00
Joe Grandja
25c4a7d541 Polish test gh-448 2021-11-03 07:56:18 -04:00
Daniel Garnier-Moiroux
088d9a8e34 Require code_verifier if code_challenge provided
Closes gh-453
2021-11-03 06:52:20 -04:00
Steve Riesenberg
defd1f90b8 Polish gh-448 2021-11-02 21:35:03 -05:00
figozhang
c9954af084 Customize authenticationDetailsSource of OAuth2TokenEndpointFilter
Closes gh-431
2021-11-02 21:32:52 -05:00
Josh Long
dc0ca22f5e Update README.adoc 2021-11-02 15:38:14 -04:00
Joe Grandja
c7f01f0795 Polish gh-427 2021-10-27 19:47:38 -04:00
Ovidiu Popa
37e45619ae Implement Client Configuration Endpoint
See: https://openid.net/specs/openid-connect-registration-1_0.html#ClientConfigurationEndpoint

Generate registration_client_uri and registration_access_token when registering a new client (see: https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration)

Closes gh-355
2021-10-25 15:00:39 -04:00
Joe Grandja
72c5e24ab8 Polish gh-441 2021-10-25 12:26:53 -04:00
Steve Riesenberg
8e8e6d1b17 Implement User Info Endpoint
Closes gh-176
2021-10-25 12:26:28 -04:00
Ido Salomon
9667229429 Initial implementation of User Info Endpoint
Issue gh-176
2021-10-25 12:25:31 -04:00
Alexey Makarov
33bac0f7c2 JdbcOAuth2AuthorizationService now uses LobCreator in findBy method
Closes gh-455
2021-10-21 19:54:41 -04:00
Joe Grandja
63c248440c Fix PKCE tests in OAuth2ClientAuthenticationProviderTests 2021-10-21 18:21:15 -04:00
Steve Riesenberg
71be32b245 Add support for deserializing LinkedHashSet
This is needed because OAuth2ClientCredentialsAuthenticationProvider stores authorized scopes in a LinkedHashSet.

Closes gh-457
2021-10-21 11:49:54 -05:00
Sarah McAlear
4b19637fbb Update RegisteredClient.Builder to use getters
- Since the class is not final, it is possible to extend it.
  Because the Builder was directly accessing the properties
  rather than using the getters, it was not possible to use
  the .from(id) constructor in the extended class.

Closes gh-451
2021-10-14 15:27:10 -04:00
Daniel Garnier-Moiroux
26f15b99bb Make OAuth2ClientAuthenticationToken @Transient 2021-10-14 15:17:57 -04:00
Joe Grandja
f3c17b3de0 Specify Jenkins user in Jenkinsfile 2021-09-23 16:00:14 -04:00
Ovidiu Popa
298ebc7c01 Avoid client secret double encoding when updating a registered client
This might have to be revisited at a later point, but to check if a value is encoded or not is quite tricky. The decision was to remove client_secret and client_secret_expires_at from the update statement

Closes gh-389
2021-09-23 13:51:56 -04:00
Joe Grandja
0735abdaad Polish gh-411 2021-09-23 12:01:42 -04:00
Dmitriy Dubson
0dfe5cb44a Fix cancel consent functionality on default consent page
- Fix also applies to custom consent sample

Closes gh-393
2021-09-23 08:17:54 -04:00
Joe Grandja
4ccdd2baf4 OAuth2TokenIntrospectionAuthenticationProvider checks for null issuer
Closes gh-438
2021-09-23 06:25:07 -04:00
Joe Grandja
e4ce97b887 Access token is active after revoke then refresh
Closes gh-432
2021-09-22 10:34:55 -04:00
Kirat Kumar
aaeca70b4c Removed an empty statement 2021-09-21 15:45:04 -04:00
Joe Grandja
8e8979af60 Next Development Version 2021-08-18 21:01:07 -04:00
Joe Grandja
725c300db2 Release 0.2.0 2021-08-18 20:14:55 -04:00
Joe Grandja
f3f69b300f Authorization failure does not clear current Authentication
Closes gh-409
2021-08-18 20:00:02 -04:00
Joe Grandja
c926884049 Update to nimbus-jose-jwt 9.10.1
Closes gh-408
2021-08-18 16:13:00 -04:00
Joe Grandja
0e99adc72e Update to jackson-bom 2.12.4
Closes gh-407
2021-08-18 16:12:47 -04:00
Joe Grandja
c444544a27 Update to Spring Boot 2.5.3
Closes gh-406
2021-08-18 16:12:20 -04:00
Joe Grandja
84c24b344f Update Reactor to 2020.0.10
Closes gh-405
2021-08-18 16:12:03 -04:00
Joe Grandja
34d4131968 Update to Spring Security 5.5.2
Closes gh-404
2021-08-18 16:11:23 -04:00
Joe Grandja
d3f25dd6ea Update to Spring Framework 5.3.9
Closes gh-403
2021-08-18 16:10:54 -04:00
Joe Grandja
f3c29bd545 Use OAuth2AuthenticationException(String errorCode)
Closes gh-402
2021-08-18 15:23:45 -04:00
Joe Grandja
ea1f95b4ed Replace stream usage with for loops
Closes gh-401
2021-08-18 13:42:08 -04:00
Joe Grandja
42d611828a Polish OAuth2TokenCustomizer 2021-08-18 11:26:12 -04:00
Joe Grandja
9388002158 Add javadoc for OAuth2TokenCustomizer
Issue gh-199
2021-08-18 10:58:05 -04:00
Anoop Garlapati
1d4dcddc11 Polish loopback address validation in DefaultRedirectUriOAuth2AuthenticationValidator
Changed loopback address validation from regex to explicit
validation using IPv4 loopback address range and IPv6 address.

Issue gh-243
2021-08-17 15:00:42 -04:00
Joe Grandja
3ee47efff7 Disable Oidc client registration by default
Closes gh-398
2021-08-17 10:04:19 -04:00
Joe Grandja
fe27e39c5d Extract configurer for OpenID Connect 1.0 support
Issue gh-398
2021-08-17 10:03:54 -04:00
Joe Grandja
57aadceb17 Remove references to experimental 2021-08-17 05:58:41 -04:00
Joe Grandja
5484931892 Update CONTRIBUTING 2021-08-17 05:42:56 -04:00
Joe Grandja
b5db5ffe54 Update README 2021-08-17 05:14:43 -04:00
Joe Grandja
9312c1807b Add support policy 2021-08-16 14:45:49 -04:00
Joe Grandja
7680505eed Move OAuth2AuthorizationCode
Closes gh-395
2021-08-13 04:56:24 -04:00
Joe Grandja
d15a68514d Polish OAuth2Authorization 2021-08-13 04:24:34 -04:00
Joe Grandja
53ed5b8481 Polish OAuth2TokenContext 2021-08-13 04:11:31 -04:00
Joe Grandja
c89f2f3819 Polish PublicClientAuthenticationConverter 2021-08-13 03:15:04 -04:00
Joe Grandja
ebecb2a7f6 Polish ClientSecretPostAuthenticationConverter 2021-08-13 03:14:25 -04:00
Joe Grandja
4995acc825 Polish ClientSecretBasicAuthenticationConverter 2021-08-13 03:13:34 -04:00
Joe Grandja
a4a61fcf50 Polish ConfigurationSettingNames 2021-08-13 02:37:03 -04:00
Joe Grandja
86997bc0ac Polish RegisteredClient 2021-08-12 17:06:13 -04:00
Joe Grandja
a740e819ae Polish OAuth2TokenRevocationAuthenticationToken 2021-08-12 16:53:29 -04:00
Joe Grandja
c7815939d2 Validate redirect_uri on dynamic client registration
Closes gh-392
2021-08-10 09:28:26 -04:00
Joe Grandja
2c8d5a19ac Remove comment in OAuth2AuthorizationCodeRequestAuthenticationProvider 2021-08-10 05:20:59 -04:00
Joe Grandja
6b5d9f0fe5 Polish JwtEncoder APIs
Closes gh-391
2021-08-10 04:49:27 -04:00
Joe Grandja
ea7c68997f Polish gh-381 2021-08-09 09:27:51 -04:00
Steve Riesenberg
115a78d5f5 Add post processor to register ProviderSettings Bean
Closes gh-373
2021-07-30 11:58:42 -04:00
Ovidiu Popa
1929e3a80a JdbcRegisteredClientRepository hashes client secret on save
Closes gh-378
2021-07-30 11:11:32 -04:00
Joe Grandja
7546d18a40 Polish gh-379 2021-07-30 09:55:29 -04:00
Steve Riesenberg
83915e8421 Do not issue refresh token to public client
Closes gh-296
2021-07-30 09:55:29 -04:00
Joe Grandja
0493bbf1d1 OAuth2ClientAuthenticationToken supports any type of credentials
Closes gh-382
2021-07-30 09:54:56 -04:00
Ovidiu Popa
41f8c9cd00 Add update support in JdbcRegisteredClientRepository
Closes gh-356
2021-07-29 11:10:36 -04:00
Joe Grandja
3d4df8807d Provide configuration for client authentication
Closes gh-380
2021-07-29 10:24:00 -04:00
Joe Grandja
850bd76aee Polish OAuth2ClientAuthenticationFilter 2021-07-29 05:55:37 -04:00
Joe Grandja
7f294abfbb Polish gh-376 2021-07-28 06:07:52 -04:00
Joe Grandja
3ea7d8c9b6 Provide configuration for refresh token generator
Closes gh-377
2021-07-28 06:02:56 -04:00
Joe Grandja
06ad211fce Provide configuration for authorization code generator
Closes gh-376
2021-07-28 04:56:05 -04:00
Joe Grandja
84e53f635c Remove Context.of()
Closes gh-375
2021-07-27 05:02:19 -04:00
Joe Grandja
f6c4d49b9f Introduce OAuth2AuthenticationValidator
Closes gh-374
2021-07-27 04:28:23 -04:00
Joe Grandja
06f2845ac0 Extract constants from Settings implementations
Closes gh-369
2021-07-23 10:50:18 -04:00
Joe Grandja
a3b14a97d6 Remove OAuth2ErrorCodes2
Closes gh-368
2021-07-23 06:25:50 -04:00
Joe Grandja
0723936b8a Remove OAuth2RefreshToken2
Closes gh-367
2021-07-23 06:18:30 -04:00
Joe Grandja
0d7727a7d4 Make Settings implementations immutable
Closes gh-366
2021-07-23 05:50:17 -04:00
Joe Grandja
79fd004346 Use OAuth2Token in OAuth2Authorization
Closes gh-364
2021-07-22 04:04:29 -04:00
Joe Grandja
70142f3705 Rename ClientSettings.requireUserConsent() to requireAuthorizationConsent()
Closes gh-363
2021-07-21 14:37:34 -04:00
Joe Grandja
c42f80c280 Remove deprecated code
Closes gh-362
2021-07-21 13:56:10 -04:00
Joe Grandja
a7feab605b Remove OAuth2ParameterNames2
Closes gh-361
2021-07-21 13:35:59 -04:00
Joe Grandja
51966d52d5 Make AuthenticationProvider implementations final
Closes gh-360
2021-07-21 11:26:57 -04:00
Joe Grandja
20d47ecaa0 Make Filter implementations final
Closes gh-359
2021-07-21 11:08:38 -04:00
Joe Grandja
d85ce0a6dd Reduce visibility of default endpoint URI constants
Closes gh-358
2021-07-21 10:42:59 -04:00
Joe Grandja
5593208e61 Move AuthenticationConverter's to web.authentication package
Closes gh-357
2021-07-21 09:32:24 -04:00
Joe Grandja
beb1233358 Rename OAuth2TokenIntrospectionClaimAccessor.getScope() to getScopes()
Closes gh-354
2021-07-21 06:34:03 -04:00
Joe Grandja
75d649578a Polish gh-350 2021-07-20 06:31:52 -04:00
Bibibiu
bd98031036 Remove use of deprecated ClientAuthenticationMethod's
Closes gh-346
2021-07-20 05:38:01 -04:00
Steve Riesenberg
687f03f047 Fix windows test failures 2021-07-14 11:11:56 -05:00
Joe Grandja
3dfcbfe136 Next Development Version 2021-07-09 08:35:47 -04:00
201 changed files with 7978 additions and 2271 deletions

View File

@@ -1,7 +1,7 @@
= Contributing to Spring Authorization Server
Spring Authorization Server is released under the Apache 2.0 license.
If you would like to contribute something, or simply want to hack on the code this document should help you https://github.com/spring-projects-experimental/spring-authorization-server#getting-started[get started].
If you would like to contribute something, or simply want to hack on the code this document should help you https://github.com/spring-projects/spring-authorization-server#getting-started[get started].
== Code of Conduct
This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct].
@@ -32,7 +32,7 @@ That may mean using an external library directly in a `Filter`.
== Reporting Security Vulnerabilities
If you think you have found a security vulnerability please *DO NOT* disclose it publicly until we've had a chance to fix it.
Please don't report security vulnerabilities using GitHub issues, instead head over to https://pivotal.io/security and learn how to disclose them responsibly.
Please don't report security vulnerabilities using GitHub issues, instead head over to https://spring.io/security-policy and learn how to disclose them responsibly.
== Sign the Contributor License Agreement
Before we accept a non-trivial patch or pull request we will need you to https://cla.pivotal.io/sign/spring[sign the Contributor License Agreement].
@@ -45,7 +45,7 @@ Please add the Apache License header to all new classes, for example:
```java
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 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.

5
Jenkinsfile vendored
View File

@@ -19,6 +19,7 @@ def OSSRH_S01_CREDENTIALS = usernamePassword(credentialsId: 'oss-s01-token', pas
def ARTIFACTORY_CREDENTIALS = usernamePassword(credentialsId: '02bd1690-b54f-4c9f-819d-a77cb7a9822c', usernameVariable: 'ARTIFACTORY_USERNAME', passwordVariable: 'ARTIFACTORY_PASSWORD')
def JENKINS_PRIVATE_SSH_KEY = file(credentialsId: 'docs.spring.io-jenkins_private_ssh_key', variable: 'DEPLOY_SSH_KEY')
def SONAR_LOGIN_CREDENTIALS = string(credentialsId: 'spring-sonar.login', variable: 'SONAR_LOGIN')
def JENKINS_USER = '-Duser.name="spring-builds+jenkins"'
def jdkEnv(String jdk = 'jdk8') {
def jdkTool = tool(jdk)
@@ -39,7 +40,7 @@ try {
"GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USERNAME}",
"GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PASSWORD}",
"GRADLE_ENTERPRISE_ACCESS_KEY=${GRADLE_ENTERPRISE_ACCESS_KEY}"]) {
sh "./gradlew check -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --stacktrace"
sh "./gradlew $JENKINS_USER check -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --stacktrace"
}
}
} catch(Exception e) {
@@ -68,7 +69,7 @@ try {
"GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USERNAME}",
"GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PASSWORD}",
"GRADLE_ENTERPRISE_ACCESS_KEY=${GRADLE_ENTERPRISE_ACCESS_KEY}"]) {
sh "./gradlew deployArtifacts finalizeDeployArtifacts -Psigning.secretKeyRingFile=$SIGNING_KEYRING_FILE -Psigning.keyId=$SPRING_SIGNING_KEYID -Psigning.password='$SIGNING_PASSWORD' -PossrhTokenUsername=$OSSRH_S01_TOKEN_USERNAME -PossrhTokenPassword=$OSSRH_S01_TOKEN_PASSWORD -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --stacktrace"
sh "./gradlew $JENKINS_USER deployArtifacts finalizeDeployArtifacts -Psigning.secretKeyRingFile=$SIGNING_KEYRING_FILE -Psigning.keyId=$SPRING_SIGNING_KEYID -Psigning.password='$SIGNING_PASSWORD' -PossrhTokenUsername=$OSSRH_S01_TOKEN_USERNAME -PossrhTokenPassword=$OSSRH_S01_TOKEN_PASSWORD -PartifactoryUsername=$ARTIFACTORY_USERNAME -PartifactoryPassword=$ARTIFACTORY_PASSWORD --stacktrace"
}
}
}

View File

@@ -1,27 +1,26 @@
image::https://badges.gitter.im/Join%20Chat.svg[Gitter,link=https://gitter.im/spring-projects/spring-security?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge]
image:https://github.com/spring-projects-experimental/spring-authorization-server/workflows/CI/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects-experimental/spring-authorization-server/actions?query=workflow%3ACI"]
image:https://github.com/spring-projects/spring-authorization-server/workflows/CI/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-authorization-server/actions?query=workflow%3ACI"]
= Spring Authorization Server
Spring Authorization Server is a community-driven project led by the https://spring.io/projects/spring-security/[Spring Security] team and is focused on delivering https://tools.ietf.org/html/rfc6749#section-1.1[OAuth 2.0 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-01#section-1.1[OAuth 2.1 Authorization Server] support to the Spring community.
The project will start in Spring's experimental projects as an independent project so that it can evolve more rapidly.
The ultimate goal of this project is to replace the Authorization Server support provided by https://spring.io/projects/spring-security-oauth/[Spring Security OAuth].
With the much needed help from our community, this project will grow in the same way that the original Spring Security OAuth project did.
This project replaces the Authorization Server support provided by https://spring.io/projects/spring-security-oauth/[Spring Security OAuth].
== Feature Planning
This project uses https://www.zenhub.com/[ZenHub] to prioritize the feature roadmap and help organize the project plan.
The project board can be accessed https://app.zenhub.com/workspaces/authorization-server-5e8f3182b5e8f5841bfc4902/board?repos=248032165[here].
It is recommended to install the ZenHub https://www.zenhub.com/extension[browser extension] as it integrates natively within GitHub's user interface.
The completed and upcoming feature list can be viewed in the https://github.com/spring-projects-experimental/spring-authorization-server/wiki/Feature-List[wiki].
The completed and upcoming feature list can be viewed in the https://github.com/spring-projects/spring-authorization-server/wiki/Feature-List[wiki].
== Support Policy
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://tools.ietf.org/html/rfc6749[OAuth 2.0 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.0 Authorization Framework and the https://github.com/spring-projects-experimental/spring-authorization-server/wiki/OAuth-2.0-Specifications[related specifications].
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.
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:
@@ -36,7 +35,7 @@ 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/site/docs/current/reference/html5/[Spring Security Reference], as well as the https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2[OAuth 2.0 Reference], which describes the Client and Resource Server features available.
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.
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].
@@ -60,7 +59,7 @@ Be sure that your `JAVA_HOME` environment variable points to the `jdk1.8.0` fold
=== Check out sources
[indent=0]
----
git clone git@github.com:spring-projects-experimental/spring-authorization-server.git
git clone git@github.com:spring-projects/spring-authorization-server.git
----

21
SUPPORT_POLICY.adoc Normal file
View File

@@ -0,0 +1,21 @@
= Spring Authorization Server Support Policy
The Spring Authorization Server support offering provides the following support terms:
* Releases are currently in the format of 0.x.y, where:
** “x” contains new features and potentially breaking changes.
** “y” contains new features and bug fixes and provides backward compatibility.
* The Spring Authorization Server project will be supported for at least 3 years after the most recent 0.x.0 release is made available for download.
* Security fixes will be provided for at least one year after the 0.x.0 release is made available for download. Security fixes will not be provided for updating versions to third-party libraries.
* Feature support and bug fixes, excluding “Security fixes”, will be provided only for the latest 0.x.y release.
* This support policy starts with version 0.2.0.
* We will switch to the standard https://tanzu.vmware.com/support/oss[Spring OSS support policy] when the Spring Authorization Server project reaches version 1.0.0.
An example can help us understand all of these points.
Assume that 0.2.0 is released in August of 2021.
This means that the Spring Authorization Server project is supported until at least August of 2024.
If 0.3.0 is then released in May of 2022, the Spring Authorization Server project is supported until at least May of 2025.
The 0.3.0 release may contain breaking changes from 0.2.0.
If a bug is found, only 0.3.0 will be patched in a 0.3.1 release.
If a security vulnerability is found, a 0.2.4 (assume 0.2.3 is latest) and 0.3.1 release will be provided to fix the security vulnerability.
However, a vulnerability found in September of 2022 would be fixed in the 0.3.1 release but not the 0.2.3 release, because the vulnerability was discovered more than a year after the 0.2.0 release date.

View File

@@ -22,7 +22,7 @@ apply plugin: 'io.spring.nohttp'
apply plugin: 'locks'
apply plugin: 'io.spring.convention.root'
group = 'org.springframework.security.experimental'
group = 'org.springframework.security'
description = 'Spring Authorization Server'
ext.snapshotBuild = version.contains("SNAPSHOT")

View File

@@ -15,7 +15,7 @@ asciidoctor {
asciidoctorj {
def ghTag = snapshotBuild ? 'main' : project.version
def ghUrl = "https://github.com/spring-projects-experimental/spring-authorization-server/tree/$ghTag"
def ghUrl = "https://github.com/spring-projects/spring-authorization-server/tree/$ghTag"
attributes 'spring-authorization-server-version' : project.version,
'spring-boot-version' : springBootVersion,
revnumber : project.version,

View File

@@ -1,5 +1,5 @@
version=0.1.2
springBootVersion=2.5.2
version=0.2.1
springBootVersion=2.5.7
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
org.gradle.caching=true

View File

@@ -1,13 +1,13 @@
if (!project.hasProperty("springVersion")) {
ext.springVersion = "5.3.8"
ext.springVersion = "5.3.13"
}
if (!project.hasProperty("springSecurityVersion")) {
ext.springSecurityVersion = "5.5.1"
ext.springSecurityVersion = "5.5.3"
}
if (!project.hasProperty("reactorVersion")) {
ext.reactorVersion = "2020.0.8"
ext.reactorVersion = "2020.0.13"
}
if (!project.hasProperty("locksDisabled")) {
@@ -21,11 +21,11 @@ dependencyManagement {
mavenBom "org.springframework:spring-framework-bom:$springVersion"
mavenBom "org.springframework.security:spring-security-bom:$springSecurityVersion"
mavenBom "io.projectreactor:reactor-bom:$reactorVersion"
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.3"
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.5"
}
dependencies {
dependency "com.nimbusds:nimbus-jose-jwt:9.8.1"
dependency "com.nimbusds:nimbus-jose-jwt:9.10.1"
dependency "javax.servlet:javax.servlet-api:4.0.1"
dependency 'junit:junit:4.13.2'
dependency 'org.assertj:assertj-core:3.19.0'

View File

@@ -32,9 +32,9 @@ import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -68,12 +68,10 @@ public class OAuth2AuthorizationServerConfiguration {
authorizeRequests.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.apply(authorizationServerConfigurer);
}
// @formatter:on
@Bean
public static JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
Set<JWSAlgorithm> jwsAlgs = new HashSet<>();
jwsAlgs.addAll(JWSAlgorithm.Family.RSA);
@@ -89,4 +87,11 @@ public class OAuth2AuthorizationServerConfiguration {
return new NimbusJwtDecoder(jwtProcessor);
}
@Bean
RegisterMissingBeanPostProcessor registerMissingBeanPostProcessor() {
RegisterMissingBeanPostProcessor postProcessor = new RegisterMissingBeanPostProcessor();
postProcessor.addBeanDefinition(ProviderSettings.class, () -> ProviderSettings.builder().build());
return postProcessor;
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configuration;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
/**
* Post processor to register one or more bean definitions on container initialization, if not already present.
*
* @author Steve Riesenberg
* @since 0.2.0
*/
final class RegisterMissingBeanPostProcessor implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
private final List<AbstractBeanDefinition> beanDefinitions = new ArrayList<>();
private BeanFactory beanFactory;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
for (AbstractBeanDefinition beanDefinition : this.beanDefinitions) {
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
(ListableBeanFactory) this.beanFactory, beanDefinition.getBeanClass(), false, false);
if (beanNames.length == 0) {
String beanName = this.beanNameGenerator.generateBeanName(beanDefinition, registry);
registry.registerBeanDefinition(beanName, beanDefinition);
}
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
<T> void addBeanDefinition(Class<T> beanClass, Supplier<T> beanSupplier) {
this.beanDefinitions.add(new RootBeanDefinition(beanClass, beanSupplier));
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}

View File

@@ -38,8 +38,12 @@ abstract class AbstractOAuth2Configurer {
abstract RequestMatcher getRequestMatcher();
protected <T> T postProcess(T object) {
protected final <T> T postProcess(T object) {
return (T) this.objectPostProcessor.postProcess(object);
}
protected final ObjectPostProcessor<Object> getObjectPostProcessor() {
return this.objectPostProcessor;
}
}

View File

@@ -130,7 +130,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
*
* <ul>
* <li>It must be an HTTP POST</li>
* <li>It must be submitted to {@link ProviderSettings#authorizationEndpoint()}</li>
* <li>It must be submitted to {@link ProviderSettings#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}
@@ -150,10 +150,10 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
this.requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(
providerSettings.authorizationEndpoint(),
providerSettings.getAuthorizationEndpoint(),
HttpMethod.GET.name()),
new AntPathRequestMatcher(
providerSettings.authorizationEndpoint(),
providerSettings.getAuthorizationEndpoint(),
HttpMethod.POST.name()));
List<AuthenticationProvider> authenticationProviders =
@@ -172,7 +172,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
new OAuth2AuthorizationEndpointFilter(
authenticationManager,
providerSettings.authorizationEndpoint());
providerSettings.getAuthorizationEndpoint());
if (this.authorizationRequestConverter != null) {
authorizationEndpointFilter.setAuthenticationConverter(this.authorizationRequestConverter);
}

View File

@@ -19,9 +19,6 @@ import java.net.URI;
import java.util.LinkedHashMap;
import java.util.Map;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
@@ -29,20 +26,14 @@ import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
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.OAuth2ClientAuthenticationFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
@@ -62,18 +53,17 @@ import org.springframework.util.Assert;
* @author Ovidiu Popa
* @since 0.0.1
* @see AbstractHttpConfigurer
* @see OAuth2ClientAuthenticationConfigurer
* @see OAuth2AuthorizationEndpointConfigurer
* @see OAuth2TokenEndpointConfigurer
* @see OidcConfigurer
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @see OAuth2AuthorizationConsentService
* @see OAuth2TokenIntrospectionEndpointFilter
* @see OAuth2TokenRevocationEndpointFilter
* @see NimbusJwkSetEndpointFilter
* @see OidcProviderConfigurationEndpointFilter
* @see OAuth2AuthorizationServerMetadataEndpointFilter
* @see OAuth2ClientAuthenticationFilter
* @see OidcClientRegistrationEndpointFilter
*/
public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBuilder<B>>
extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer<B>, B> {
@@ -82,18 +72,15 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
private RequestMatcher tokenIntrospectionEndpointMatcher;
private RequestMatcher tokenRevocationEndpointMatcher;
private RequestMatcher jwkSetEndpointMatcher;
private RequestMatcher oidcProviderConfigurationEndpointMatcher;
private RequestMatcher authorizationServerMetadataEndpointMatcher;
private RequestMatcher oidcClientRegistrationEndpointMatcher;
private final RequestMatcher endpointsMatcher = (request) ->
getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) ||
getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) ||
getRequestMatcher(OidcConfigurer.class).matches(request) ||
this.tokenIntrospectionEndpointMatcher.matches(request) ||
this.tokenRevocationEndpointMatcher.matches(request) ||
this.jwkSetEndpointMatcher.matches(request) ||
this.oidcProviderConfigurationEndpointMatcher.matches(request) ||
this.authorizationServerMetadataEndpointMatcher.matches(request) ||
this.oidcClientRegistrationEndpointMatcher.matches(request);
this.authorizationServerMetadataEndpointMatcher.matches(request);
/**
* Sets the repository of registered clients.
@@ -103,7 +90,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
*/
public OAuth2AuthorizationServerConfigurer<B> registeredClientRepository(RegisteredClientRepository registeredClientRepository) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
this.getBuilder().setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
getBuilder().setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
return this;
}
@@ -115,7 +102,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
*/
public OAuth2AuthorizationServerConfigurer<B> authorizationService(OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.getBuilder().setSharedObject(OAuth2AuthorizationService.class, authorizationService);
getBuilder().setSharedObject(OAuth2AuthorizationService.class, authorizationService);
return this;
}
@@ -127,7 +114,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
*/
public OAuth2AuthorizationServerConfigurer<B> authorizationConsentService(OAuth2AuthorizationConsentService authorizationConsentService) {
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
this.getBuilder().setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
getBuilder().setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
return this;
}
@@ -139,7 +126,18 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
*/
public OAuth2AuthorizationServerConfigurer<B> providerSettings(ProviderSettings providerSettings) {
Assert.notNull(providerSettings, "providerSettings cannot be null");
this.getBuilder().setSharedObject(ProviderSettings.class, providerSettings);
getBuilder().setSharedObject(ProviderSettings.class, providerSettings);
return this;
}
/**
* Configures OAuth 2.0 Client Authentication.
*
* @param clientAuthenticationCustomizer the {@link Customizer} providing access to the {@link OAuth2ClientAuthenticationConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer<B> clientAuthentication(Customizer<OAuth2ClientAuthenticationConfigurer> clientAuthenticationCustomizer) {
clientAuthenticationCustomizer.customize(getConfigurer(OAuth2ClientAuthenticationConfigurer.class));
return this;
}
@@ -165,6 +163,17 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
return this;
}
/**
* Configures OpenID Connect 1.0 support.
*
* @param oidcCustomizer the {@link Customizer} providing access to the {@link OidcConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer<B> oidc(Customizer<OidcConfigurer> oidcCustomizer) {
oidcCustomizer.customize(getConfigurer(OidcConfigurer.class));
return this;
}
/**
* Returns a {@link RequestMatcher} for the authorization server endpoints.
*
@@ -182,16 +191,6 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
this.configurers.values().forEach(configurer -> configurer.init(builder));
OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
new OAuth2ClientAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
OAuth2ConfigurerUtils.getAuthorizationService(builder));
PasswordEncoder passwordEncoder = OAuth2ConfigurerUtils.getOptionalBean(builder, PasswordEncoder.class);
if (passwordEncoder != null) {
clientAuthenticationProvider.setPasswordEncoder(passwordEncoder);
}
builder.authenticationProvider(postProcess(clientAuthenticationProvider));
OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =
new OAuth2TokenIntrospectionAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
@@ -203,13 +202,6 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
OAuth2ConfigurerUtils.getAuthorizationService(builder));
builder.authenticationProvider(postProcess(tokenRevocationAuthenticationProvider));
// TODO Make OpenID Client Registration an "opt-in" feature
OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider =
new OidcClientRegistrationAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
OAuth2ConfigurerUtils.getAuthorizationService(builder));
builder.authenticationProvider(postProcess(oidcClientRegistrationAuthenticationProvider));
ExceptionHandlingConfigurer<B> exceptionHandling = builder.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionHandling != null) {
exceptionHandling.defaultAuthenticationEntryPointFor(
@@ -227,57 +219,39 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
this.configurers.values().forEach(configurer -> configurer.configure(builder));
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
if (providerSettings.issuer() != null) {
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
new OidcProviderConfigurationEndpointFilter(providerSettings);
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings);
builder.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
JWKSource<SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(builder);
NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
jwkSource,
providerSettings.jwkSetEndpoint());
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
OAuth2ClientAuthenticationFilter clientAuthenticationFilter =
new OAuth2ClientAuthenticationFilter(
authenticationManager,
new OrRequestMatcher(
getRequestMatcher(OAuth2TokenEndpointConfigurer.class),
this.tokenIntrospectionEndpointMatcher,
this.tokenRevocationEndpointMatcher));
builder.addFilterAfter(postProcess(clientAuthenticationFilter), AbstractPreAuthenticatedProcessingFilter.class);
OAuth2TokenIntrospectionEndpointFilter tokenIntrospectionEndpointFilter =
new OAuth2TokenIntrospectionEndpointFilter(
authenticationManager,
providerSettings.tokenIntrospectionEndpoint());
providerSettings.getTokenIntrospectionEndpoint());
builder.addFilterAfter(postProcess(tokenIntrospectionEndpointFilter), FilterSecurityInterceptor.class);
OAuth2TokenRevocationEndpointFilter tokenRevocationEndpointFilter =
new OAuth2TokenRevocationEndpointFilter(
authenticationManager,
providerSettings.tokenRevocationEndpoint());
builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), OAuth2TokenIntrospectionEndpointFilter.class);
providerSettings.getTokenRevocationEndpoint());
builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), FilterSecurityInterceptor.class);
// TODO Make OpenID Client Registration an "opt-in" feature
OidcClientRegistrationEndpointFilter oidcClientRegistrationEndpointFilter =
new OidcClientRegistrationEndpointFilter(
authenticationManager,
providerSettings.oidcClientRegistrationEndpoint());
builder.addFilterAfter(postProcess(oidcClientRegistrationEndpointFilter), OAuth2TokenRevocationEndpointFilter.class);
NimbusJwkSetEndpointFilter jwkSetEndpointFilter =
new NimbusJwkSetEndpointFilter(
OAuth2ConfigurerUtils.getJwkSource(builder),
providerSettings.getJwkSetEndpoint());
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
if (providerSettings.getIssuer() != null) {
OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings);
builder.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(OAuth2AuthorizationEndpointConfigurer.class, new OAuth2AuthorizationEndpointConfigurer(this::postProcess));
configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
configurers.put(OidcConfigurer.class, new OidcConfigurer(this::postProcess));
return configurers;
}
@@ -292,23 +266,19 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
private void initEndpointMatchers(ProviderSettings providerSettings) {
this.tokenIntrospectionEndpointMatcher = new AntPathRequestMatcher(
providerSettings.tokenIntrospectionEndpoint(), HttpMethod.POST.name());
providerSettings.getTokenIntrospectionEndpoint(), HttpMethod.POST.name());
this.tokenRevocationEndpointMatcher = new AntPathRequestMatcher(
providerSettings.tokenRevocationEndpoint(), HttpMethod.POST.name());
providerSettings.getTokenRevocationEndpoint(), HttpMethod.POST.name());
this.jwkSetEndpointMatcher = new AntPathRequestMatcher(
providerSettings.jwkSetEndpoint(), HttpMethod.GET.name());
this.oidcProviderConfigurationEndpointMatcher = new AntPathRequestMatcher(
OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
providerSettings.getJwkSetEndpoint(), HttpMethod.GET.name());
this.authorizationServerMetadataEndpointMatcher = new AntPathRequestMatcher(
OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI, HttpMethod.GET.name());
this.oidcClientRegistrationEndpointMatcher = new AntPathRequestMatcher(
providerSettings.oidcClientRegistrationEndpoint(), HttpMethod.POST.name());
"/.well-known/oauth-authorization-server", HttpMethod.GET.name());
}
private static void validateProviderSettings(ProviderSettings providerSettings) {
if (providerSettings.issuer() != null) {
if (providerSettings.getIssuer() != null) {
try {
new URI(providerSettings.issuer()).toURL();
new URI(providerSettings.getIssuer()).toURL();
} catch (Exception ex) {
throw new IllegalArgumentException("issuer must be a valid URL", ex);
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
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.HttpSecurityBuilder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
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.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Configurer for OAuth 2.0 Client Authentication.
*
* @author Joe Grandja
* @since 0.2.0
* @see OAuth2AuthorizationServerConfigurer#clientAuthentication
* @see OAuth2ClientAuthenticationFilter
*/
public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private AuthenticationConverter authenticationConverter;
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private AuthenticationSuccessHandler authenticationSuccessHandler;
private AuthenticationFailureHandler errorResponseHandler;
/**
* Restrict for internal use only.
*/
OAuth2ClientAuthenticationConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Sets the {@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}
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer authenticationConverter(AuthenticationConverter authenticationConverter) {
this.authenticationConverter = authenticationConverter;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an {@link OAuth2ClientAuthenticationToken}.
*
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OAuth2ClientAuthenticationToken}
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling a successful client authentication
* and associating the {@link OAuth2ClientAuthenticationToken} to the {@link SecurityContext}.
*
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling a successful client authentication
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer authenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
this.authenticationSuccessHandler = authenticationSuccessHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling a failed client authentication
* and returning the {@link OAuth2Error Error Response}.
*
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling a failed client authentication
* @return the {@link OAuth2ClientAuthenticationConfigurer} for further configuration
*/
public OAuth2ClientAuthenticationConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
@Override
<B extends HttpSecurityBuilder<B>> void init(B builder) {
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
this.requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(
providerSettings.getTokenEndpoint(),
HttpMethod.POST.name()),
new AntPathRequestMatcher(
providerSettings.getTokenIntrospectionEndpoint(),
HttpMethod.POST.name()),
new AntPathRequestMatcher(
providerSettings.getTokenRevocationEndpoint(),
HttpMethod.POST.name()));
List<AuthenticationProvider> authenticationProviders =
!this.authenticationProviders.isEmpty() ?
this.authenticationProviders :
createDefaultAuthenticationProviders(builder);
authenticationProviders.forEach(authenticationProvider ->
builder.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
OAuth2ClientAuthenticationFilter clientAuthenticationFilter = new OAuth2ClientAuthenticationFilter(
authenticationManager, this.requestMatcher);
if (this.authenticationConverter != null) {
clientAuthenticationFilter.setAuthenticationConverter(this.authenticationConverter);
}
if (this.authenticationSuccessHandler != null) {
clientAuthenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
}
if (this.errorResponseHandler != null) {
clientAuthenticationFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
builder.addFilterAfter(postProcess(clientAuthenticationFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
new OAuth2ClientAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
OAuth2ConfigurerUtils.getAuthorizationService(builder));
PasswordEncoder passwordEncoder = OAuth2ConfigurerUtils.getOptionalBean(builder, PasswordEncoder.class);
if (passwordEncoder != null) {
clientAuthenticationProvider.setPasswordEncoder(passwordEncoder);
}
authenticationProviders.add(clientAuthenticationProvider);
return authenticationProviders;
}
}

View File

@@ -124,7 +124,7 @@ final class OAuth2ConfigurerUtils {
if (providerSettings == null) {
providerSettings = getOptionalBean(builder, ProviderSettings.class);
if (providerSettings == null) {
providerSettings = new ProviderSettings();
providerSettings = ProviderSettings.builder().build();
}
builder.setSharedObject(ProviderSettings.class, providerSettings);
}

View File

@@ -119,7 +119,7 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
<B extends HttpSecurityBuilder<B>> void init(B builder) {
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
this.requestMatcher = new AntPathRequestMatcher(
providerSettings.tokenEndpoint(), HttpMethod.POST.name());
providerSettings.getTokenEndpoint(), HttpMethod.POST.name());
List<AuthenticationProvider> authenticationProviders =
!this.authenticationProviders.isEmpty() ?
@@ -137,7 +137,7 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
OAuth2TokenEndpointFilter tokenEndpointFilter =
new OAuth2TokenEndpointFilter(
authenticationManager,
providerSettings.tokenEndpoint());
providerSettings.getTokenEndpoint());
if (this.accessTokenRequestConverter != null) {
tokenEndpointFilter.setAuthenticationConverter(this.accessTokenRequestConverter);
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcClientRegistrationEndpointFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Configurer for OpenID Connect Dynamic Client Registration 1.0 Endpoint.
*
* @author Joe Grandja
* @since 0.2.0
* @see OidcConfigurer#clientRegistrationEndpoint
* @see OidcClientRegistrationEndpointFilter
*/
public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
/**
* Restrict for internal use only.
*/
OidcClientRegistrationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
@Override
<B extends HttpSecurityBuilder<B>> void init(B builder) {
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
this.requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(providerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.POST.name()),
new AntPathRequestMatcher(providerSettings.getOidcClientRegistrationEndpoint(), HttpMethod.GET.name())
);
OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider =
new OidcClientRegistrationAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
OAuth2ConfigurerUtils.getAuthorizationService(builder),
OAuth2ConfigurerUtils.getJwtEncoder(builder));
builder.authenticationProvider(postProcess(oidcClientRegistrationAuthenticationProvider));
}
@Override
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
OidcClientRegistrationEndpointFilter oidcClientRegistrationEndpointFilter =
new OidcClientRegistrationEndpointFilter(
authenticationManager,
providerSettings.getOidcClientRegistrationEndpoint());
builder.addFilterAfter(postProcess(oidcClientRegistrationEndpointFilter), FilterSecurityInterceptor.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
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.HttpSecurityBuilder;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
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.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Configurer for OpenID Connect 1.0 support.
*
* @author Joe Grandja
* @since 0.2.0
* @see OAuth2AuthorizationServerConfigurer#oidc
* @see OidcClientRegistrationEndpointConfigurer
* @see OidcUserInfoEndpointConfigurer
* @see OidcProviderConfigurationEndpointFilter
*/
public final class OidcConfigurer extends AbstractOAuth2Configurer {
private final OidcUserInfoEndpointConfigurer userInfoEndpointConfigurer;
private OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer;
private RequestMatcher requestMatcher;
/**
* Restrict for internal use only.
*/
OidcConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
this.userInfoEndpointConfigurer = new OidcUserInfoEndpointConfigurer(objectPostProcessor);
}
/**
* Configures the OpenID Connect Dynamic Client Registration 1.0 Endpoint.
*
* @param clientRegistrationEndpointCustomizer the {@link Customizer} providing access to the {@link OidcClientRegistrationEndpointConfigurer}
* @return the {@link OidcConfigurer} for further configuration
*/
public OidcConfigurer clientRegistrationEndpoint(Customizer<OidcClientRegistrationEndpointConfigurer> clientRegistrationEndpointCustomizer) {
if (this.clientRegistrationEndpointConfigurer == null) {
this.clientRegistrationEndpointConfigurer = new OidcClientRegistrationEndpointConfigurer(getObjectPostProcessor());
}
clientRegistrationEndpointCustomizer.customize(this.clientRegistrationEndpointConfigurer);
return this;
}
/**
* Configures the OpenID Connect 1.0 UserInfo Endpoint.
*
* @param userInfoEndpointCustomizer the {@link Customizer} providing access to the {@link OidcUserInfoEndpointConfigurer}
* @return the {@link OidcConfigurer} for further configuration
*/
public OidcConfigurer userInfoEndpoint(Customizer<OidcUserInfoEndpointConfigurer> userInfoEndpointCustomizer) {
userInfoEndpointCustomizer.customize(this.userInfoEndpointConfigurer);
return this;
}
@Override
<B extends HttpSecurityBuilder<B>> void init(B builder) {
this.userInfoEndpointConfigurer.init(builder);
if (this.clientRegistrationEndpointConfigurer != null) {
this.clientRegistrationEndpointConfigurer.init(builder);
}
List<RequestMatcher> requestMatchers = new ArrayList<>();
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
if (providerSettings.getIssuer() != null) {
requestMatchers.add(new AntPathRequestMatcher(
"/.well-known/openid-configuration", HttpMethod.GET.name()));
}
requestMatchers.add(this.userInfoEndpointConfigurer.getRequestMatcher());
if (this.clientRegistrationEndpointConfigurer != null) {
requestMatchers.add(this.clientRegistrationEndpointConfigurer.getRequestMatcher());
}
this.requestMatcher = requestMatchers.size() > 1 ? new OrRequestMatcher(requestMatchers) : requestMatchers.get(0);
}
@Override
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
this.userInfoEndpointConfigurer.configure(builder);
if (this.clientRegistrationEndpointConfigurer != null) {
this.clientRegistrationEndpointConfigurer.configure(builder);
}
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
if (providerSettings.getIssuer() != null) {
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
new OidcProviderConfigurationEndpointFilter(providerSettings);
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.function.Function;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
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.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Configurer for OpenID Connect 1.0 UserInfo Endpoint.
*
* @author Steve Riesenberg
* @since 0.2.1
* @see OidcConfigurer#userInfoEndpoint
* @see OidcUserInfoEndpointFilter
*/
public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper;
/**
* Restrict for internal use only.
*/
OidcUserInfoEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Sets the {@link Function} used to extract claims from {@link OidcUserInfoAuthenticationContext}
* to an instance of {@link OidcUserInfo} for the UserInfo response.
*
* <p>
* The {@link OidcUserInfoAuthenticationContext} gives the mapper access to the {@link OidcUserInfoAuthenticationToken},
* as well as, the following context attributes:
* <ul>
* <li>{@link OidcUserInfoAuthenticationContext#getAccessToken()} containing the bearer token used to make the request.</li>
* <li>{@link OidcUserInfoAuthenticationContext#getAuthorization()} containing the {@link OidcIdToken} and
* {@link OAuth2AccessToken} associated with the bearer token used to make the request.</li>
* </ul>
*
* @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) {
this.userInfoMapper = userInfoMapper;
return this;
}
@Override
<B extends HttpSecurityBuilder<B>> void init(B builder) {
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
String userInfoEndpointUri = providerSettings.getOidcUserInfoEndpoint();
this.requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.GET.name()),
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.POST.name()));
OidcUserInfoAuthenticationProvider oidcUserInfoAuthenticationProvider =
new OidcUserInfoAuthenticationProvider(
OAuth2ConfigurerUtils.getAuthorizationService(builder));
if (this.userInfoMapper != null) {
oidcUserInfoAuthenticationProvider.setUserInfoMapper(this.userInfoMapper);
}
builder.authenticationProvider(postProcess(oidcUserInfoAuthenticationProvider));
}
@Override
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
OidcUserInfoEndpointFilter oidcUserInfoEndpointFilter =
new OidcUserInfoEndpointFilter(
authenticationManager,
providerSettings.getOidcUserInfoEndpoint());
builder.addFilterAfter(postProcess(oidcUserInfoEndpointFilter), FilterSecurityInterceptor.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
}

View File

@@ -73,7 +73,7 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth
@SuppressWarnings("unchecked")
protected final B getThis() {
return (B) this; // avoid unchecked casts in subclasses by using "getThis()" instead of "(B) this"
};
}
/**
* Use this {@code issuer} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 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.
@@ -13,9 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
package org.springframework.security.oauth2.core;
import java.time.Instant;

View File

@@ -1,30 +0,0 @@
/*
* Copyright 2020 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.core;
/**
* TODO
* This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA.
*
* @author Joe Grandja
* @since 0.0.3
* @see <a target="_blank" href="https://github.com/spring-projects/spring-security/issues/9184">Issue gh-9184</a>
*/
public interface OAuth2ErrorCodes2 extends OAuth2ErrorCodes {
String UNSUPPORTED_TOKEN_TYPE = "unsupported_token_type";
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2020 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.core;
import java.time.Instant;
/**
* TODO
* This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA.
*
* @author Joe Grandja
* @since 0.0.3
* @see <a target="_blank" href="https://github.com/spring-projects/spring-security/pull/9146">Issue gh-9146</a>
*/
public class OAuth2RefreshToken2 extends OAuth2RefreshToken {
private final Instant expiresAt;
public OAuth2RefreshToken2(String tokenValue, Instant issuedAt, Instant expiresAt) {
super(tokenValue, issuedAt);
this.expiresAt = expiresAt;
}
@Override
public Instant getExpiresAt() {
return this.expiresAt;
}
}

View File

@@ -56,7 +56,7 @@ public interface OAuth2TokenIntrospectionClaimAccessor extends ClaimAccessor {
* Returns the scopes {@code (scope)} associated with the token
* @return the scopes associated with the token
*/
default List<String> getScope() {
default List<String> getScopes() {
return getClaimAsStringList(OAuth2TokenIntrospectionClaimNames.SCOPE);
}

View File

@@ -23,8 +23,8 @@ package org.springframework.security.oauth2.core;
*/
public final class Version {
private static final int MAJOR = 0;
private static final int MINOR = 1;
private static final int PATCH = 2;
private static final int MINOR = 2;
private static final int PATCH = 1;
/**
* Global Serialization value for Spring Security Authorization Server classes.

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2020-2021 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.core.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.core.Authentication;
import org.springframework.security.oauth2.core.context.Context;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* A context that holds an {@link Authentication} and (optionally) additional information.
*
* @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));
}
/**
* Returns the {@link Authentication} associated to the context.
*
* @param <T> the type of the {@code Authentication}
* @return the {@link Authentication}
*/
@SuppressWarnings("unchecked")
public <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}.
*
* @param <T> the type of the authentication 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>> {
private final Map<Object, Object> context = new HashMap<>();
protected AbstractBuilder(Authentication authentication) {
Assert.notNull(authentication, "authentication cannot be null");
put(Authentication.class, authentication);
}
/**
* Associates an attribute.
*
* @param key the key for the attribute
* @param value the value of the attribute
* @return the {@link AbstractBuilder} for further configuration
*/
public B put(Object key, Object value) {
Assert.notNull(key, "key cannot be null");
Assert.notNull(value, "value cannot be null");
getContext().put(key, value);
return getThis();
}
/**
* A {@code Consumer} of the attributes {@code Map}
* allowing the ability to add, replace, or remove.
*
* @param contextConsumer a {@link Consumer} of the attributes {@code Map}
* @return the {@link AbstractBuilder} for further configuration
*/
public B context(Consumer<Map<Object, Object>> contextConsumer) {
contextConsumer.accept(getContext());
return getThis();
}
@SuppressWarnings("unchecked")
protected <V> V get(Object key) {
return (V) getContext().get(key);
}
protected Map<Object, Object> getContext() {
return this.context;
}
@SuppressWarnings("unchecked")
protected final B getThis() {
return (B) this;
}
/**
* Builds a new {@link OAuth2AuthenticationContext}.
*
* @return the {@link OAuth2AuthenticationContext}
*/
public abstract T build();
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2020-2021 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.core.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;
}

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.security.oauth2.core.context;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -28,9 +26,23 @@ import org.springframework.util.Assert;
*/
public interface Context {
/**
* Returns the value of the attribute associated to the key.
*
* @param key the key for the attribute
* @param <V> the type of the value for the attribute
* @return the value of the attribute associated to the key, or {@code null} if not available
*/
@Nullable
<V> V get(Object key);
/**
* Returns the value of the attribute associated to the key.
*
* @param key the key for the attribute
* @param <V> the type of the value for the attribute
* @return the value of the attribute associated to the key, or {@code null} if not available or not of the specified type
*/
@Nullable
default <V> V get(Class<V> key) {
Assert.notNull(key, "key cannot be null");
@@ -38,10 +50,12 @@ public interface Context {
return key.isInstance(value) ? value : null;
}
/**
* Returns {@code true} if an attribute associated to the key exists, {@code false} otherwise.
*
* @param key the key for the attribute
* @return {@code true} if an attribute associated to the key exists, {@code false} otherwise
*/
boolean hasKey(Object key);
static Context of(Map<Object, Object> context) {
return new DefaultContext(context);
}
}

View File

@@ -1,50 +0,0 @@
/*
* Copyright 2020-2021 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.core.context;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* @author Joe Grandja
* @since 0.1.0
*/
final class DefaultContext implements Context {
private final Map<Object, Object> context;
DefaultContext(Map<Object, Object> context) {
Assert.notNull(context, "context cannot be null");
this.context = Collections.unmodifiableMap(new HashMap<>(context));
}
@SuppressWarnings("unchecked")
@Override
@Nullable
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);
}
}

View File

@@ -1,32 +0,0 @@
/*
* Copyright 2020 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.core.endpoint;
/**
* TODO
* This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA.
*
* @author Joe Grandja
* @since 0.0.3
* @see <a target="_blank" href="https://github.com/spring-projects/spring-security/issues/9183">Issue gh-9183</a>
*/
public interface OAuth2ParameterNames2 extends OAuth2ParameterNames {
String TOKEN = "token";
String TOKEN_TYPE_HINT = "token_type_hint";
}

View File

@@ -184,8 +184,8 @@ public class OAuth2TokenIntrospectionHttpMessageConverter extends AbstractHttpMe
@Override
public Map<String, Object> convert(OAuth2TokenIntrospection source) {
Map<String, Object> responseClaims = new LinkedHashMap<>(source.getClaims());
if (!CollectionUtils.isEmpty(source.getScope())) {
responseClaims.put(OAuth2TokenIntrospectionClaimNames.SCOPE, StringUtils.collectionToDelimitedString(source.getScope(), " "));
if (!CollectionUtils.isEmpty(source.getScopes())) {
responseClaims.put(OAuth2TokenIntrospectionClaimNames.SCOPE, StringUtils.collectionToDelimitedString(source.getScopes(), " "));
}
if (source.getExpiresAt() != null) {
responseClaims.put(OAuth2TokenIntrospectionClaimNames.EXP, source.getExpiresAt().getEpochSecond());

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.security.oauth2.core.oidc;
import java.net.URL;
import java.time.Instant;
import java.util.List;
@@ -134,4 +135,24 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
return getClaimAsString(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG);
}
/**
* Returns the Registration Access Token that can be used at the Client Configuration Endpoint.
*
* @return the Registration Access Token that can be used at the Client Configuration Endpoint
* @since 0.2.1
*/
default String getRegistrationAccessToken() {
return getClaimAsString(OidcClientMetadataClaimNames.REGISTRATION_ACCESS_TOKEN);
}
/**
* Returns the {@code URL} of the Client Configuration Endpoint where the Registration Access Token can be used.
*
* @return the {@code URL} of the Client Configuration Endpoint where the Registration Access Token can be used
* @since 0.2.1
*/
default URL getRegistrationClientUrl() {
return getClaimAsURL(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI);
}
}

View File

@@ -83,4 +83,16 @@ public interface OidcClientMetadataClaimNames {
*/
String ID_TOKEN_SIGNED_RESPONSE_ALG = "id_token_signed_response_alg";
/**
* {@code registration_access_token} - the Registration Access Token that can be used at the Client Configuration Endpoint
* @since 0.2.1
*/
String REGISTRATION_ACCESS_TOKEN = "registration_access_token";
/**
* {@code registration_client_uri} - the {@code URL} of the Client Configuration Endpoint where the Registration Access Token can be used
* @since 0.2.1
*/
String REGISTRATION_CLIENT_URI = "registration_client_uri";
}

View File

@@ -16,8 +16,6 @@
package org.springframework.security.oauth2.core.oidc;
import java.io.Serializable;
import java.net.URI;
import java.net.URL;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -253,6 +251,28 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
return claim(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, idTokenSignedResponseAlgorithm);
}
/**
* Sets the Registration Access Token that can be used at the Client Configuration Endpoint, OPTIONAL.
*
* @param registrationAccessToken the Registration Access Token that can be used at the Client Configuration Endpoint
* @return the {@link Builder} for further configuration
* @since 0.2.1
*/
public Builder registrationAccessToken(String registrationAccessToken) {
return claim(OidcClientMetadataClaimNames.REGISTRATION_ACCESS_TOKEN, registrationAccessToken);
}
/**
* Sets the {@code URL} of the Client Configuration Endpoint where the Registration Access Token can be used, OPTIONAL.
*
* @param registrationClientUrl the {@code URL} of the Client Configuration Endpoint where the Registration Access Token can be used
* @return the {@link Builder} for further configuration
* @since 0.2.1
*/
public Builder registrationClientUrl(String registrationClientUrl) {
return claim(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI, registrationClientUrl);
}
/**
* Sets the claim.
*
@@ -307,9 +327,6 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
Assert.notNull(this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris cannot be null");
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris cannot be empty");
((List<?>) this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS)).forEach(
url -> validateURL(url, "redirect_uri must be a valid URL")
);
if (this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES) != null) {
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES), "grant_types must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES), "grant_types cannot be empty");
@@ -341,15 +358,5 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
valuesConsumer.accept(values);
}
private static void validateURL(Object url, String errorMessage) {
if (URL.class.isAssignableFrom(url.getClass())) {
return;
}
try {
new URI(url.toString()).toURL();
} catch (Exception ex) {
throw new IllegalArgumentException(errorMessage, ex);
}
}
}
}

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2020-2021 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.core.oidc.http.converter;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
import org.springframework.util.Assert;
/**
* A {@link HttpMessageConverter} for an {@link OidcUserInfo OpenID Connect UserInfo Response}.
*
* @author Ido Salomon
* @author Steve Riesenberg
* @since 0.2.1
* @see AbstractHttpMessageConverter
* @see OidcUserInfo
*/
public class OidcUserInfoHttpMessageConverter extends AbstractHttpMessageConverter<OidcUserInfo> {
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP =
new ParameterizedTypeReference<Map<String, Object>>() {};
private final GenericHttpMessageConverter<Object> jsonMessageConverter =
HttpMessageConverters.getJsonMessageConverter();
private Converter<Map<String, Object>, OidcUserInfo> userInfoConverter = new MapOidcUserInfoConverter();
private Converter<OidcUserInfo, Map<String, Object>> userInfoParametersConverter = OidcUserInfo::getClaims;
public OidcUserInfoHttpMessageConverter() {
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
}
@Override
protected boolean supports(Class<?> clazz) {
return OidcUserInfo.class.isAssignableFrom(clazz);
}
@Override
@SuppressWarnings("unchecked")
protected OidcUserInfo readInternal(Class<? extends OidcUserInfo> clazz, HttpInputMessage inputMessage)
throws HttpMessageNotReadableException {
try {
Map<String, Object> userInfoParameters =
(Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
return this.userInfoConverter.convert(userInfoParameters);
} catch (Exception ex) {
throw new HttpMessageNotReadableException(
"An error occurred reading the UserInfo response: " + ex.getMessage(), ex, inputMessage);
}
}
@Override
protected void writeInternal(OidcUserInfo oidcUserInfo, HttpOutputMessage outputMessage)
throws HttpMessageNotWritableException {
try {
Map<String, Object> userInfoResponseParameters =
this.userInfoParametersConverter.convert(oidcUserInfo);
this.jsonMessageConverter.write(
userInfoResponseParameters,
STRING_OBJECT_MAP.getType(),
MediaType.APPLICATION_JSON,
outputMessage
);
} catch (Exception ex) {
throw new HttpMessageNotWritableException(
"An error occurred writing the UserInfo response: " + ex.getMessage(), ex);
}
}
/**
* Sets the {@link Converter} used for converting the UserInfo parameters
* to an {@link OidcUserInfo}.
*
* @param userInfoConverter the {@link Converter} used for converting to an {@link OidcUserInfo}
*/
public final void setUserInfoConverter(Converter<Map<String, Object>, OidcUserInfo> userInfoConverter) {
Assert.notNull(userInfoConverter, "userInfoConverter cannot be null");
this.userInfoConverter = userInfoConverter;
}
/**
* Sets the {@link Converter} used for converting the {@link OidcUserInfo} to a
* {@code Map} representation of the UserInfo.
*
* @param userInfoParametersConverter the {@link Converter} used for converting to a
* {@code Map} representation of the UserInfo
*/
public final void setUserInfoParametersConverter(
Converter<OidcUserInfo, Map<String, Object>> userInfoParametersConverter) {
Assert.notNull(userInfoParametersConverter, "userInfoParametersConverter cannot be null");
this.userInfoParametersConverter = userInfoParametersConverter;
}
private static final class MapOidcUserInfoConverter implements Converter<Map<String, Object>, OidcUserInfo> {
private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance();
private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
private static final TypeDescriptor BOOLEAN_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Boolean.class);
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class);
private static final TypeDescriptor STRING_OBJECT_MAP_DESCRIPTOR = TypeDescriptor.map(Map.class, STRING_TYPE_DESCRIPTOR, OBJECT_TYPE_DESCRIPTOR);
private final ClaimTypeConverter claimTypeConverter;
private MapOidcUserInfoConverter() {
Converter<Object, ?> booleanConverter = getConverter(BOOLEAN_TYPE_DESCRIPTOR);
Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
Converter<Object, ?> instantConverter = getConverter(INSTANT_TYPE_DESCRIPTOR);
Converter<Object, ?> mapConverter = getConverter(STRING_OBJECT_MAP_DESCRIPTOR);
Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
claimConverters.put(StandardClaimNames.SUB, stringConverter);
claimConverters.put(StandardClaimNames.NAME, stringConverter);
claimConverters.put(StandardClaimNames.GIVEN_NAME, stringConverter);
claimConverters.put(StandardClaimNames.FAMILY_NAME, stringConverter);
claimConverters.put(StandardClaimNames.MIDDLE_NAME, stringConverter);
claimConverters.put(StandardClaimNames.NICKNAME, stringConverter);
claimConverters.put(StandardClaimNames.PREFERRED_USERNAME, stringConverter);
claimConverters.put(StandardClaimNames.PROFILE, stringConverter);
claimConverters.put(StandardClaimNames.PICTURE, stringConverter);
claimConverters.put(StandardClaimNames.WEBSITE, stringConverter);
claimConverters.put(StandardClaimNames.EMAIL, stringConverter);
claimConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
claimConverters.put(StandardClaimNames.GENDER, stringConverter);
claimConverters.put(StandardClaimNames.BIRTHDATE, stringConverter);
claimConverters.put(StandardClaimNames.ZONEINFO, stringConverter);
claimConverters.put(StandardClaimNames.LOCALE, stringConverter);
claimConverters.put(StandardClaimNames.PHONE_NUMBER, stringConverter);
claimConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
claimConverters.put(StandardClaimNames.ADDRESS, mapConverter);
claimConverters.put(StandardClaimNames.UPDATED_AT, instantConverter);
this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
}
@Override
public OidcUserInfo convert(Map<String, Object> source) {
Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
return new OidcUserInfo(parsedClaims);
}
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
}
}
}

View File

@@ -24,7 +24,7 @@ import java.util.Set;
import java.util.function.Consumer;
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.JwaAlgorithm;
import org.springframework.util.Assert;
/**
@@ -36,9 +36,9 @@ import org.springframework.util.Assert;
* @author Joe Grandja
* @since 0.0.1
* @see Jwt
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-5">JWT JOSE Header</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-4">JWS JOSE Header</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-4">JWE JOSE Header</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7519#section-5">JWT JOSE Header</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7515#section-4">JWS JOSE Header</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7516#section-4">JWE JOSE Header</a>
*/
public final class JoseHeader {
private final Map<String, Object> headers;
@@ -48,12 +48,13 @@ public final class JoseHeader {
}
/**
* Returns the {@link JwsAlgorithm JWS algorithm} used to digitally sign the JWS.
* Returns the {@link JwaAlgorithm JWA algorithm} used to digitally sign the JWS or encrypt the JWE.
*
* @return the JWS algorithm
* @return the {@link JwaAlgorithm}
*/
public JwsAlgorithm getJwsAlgorithm() {
return getHeader(JoseHeaderNames.ALG);
@SuppressWarnings("unchecked")
public <T extends JwaAlgorithm> T getAlgorithm() {
return (T) getHeader(JoseHeaderNames.ALG);
}
/**
@@ -62,7 +63,7 @@ public final class JoseHeader {
*
* @return the JWK Set URL
*/
public URL getJwkSetUri() {
public URL getJwkSetUrl() {
return getHeader(JoseHeaderNames.JKU);
}
@@ -91,13 +92,16 @@ public final class JoseHeader {
*
* @return the X.509 URL
*/
public URL getX509Uri() {
public URL getX509Url() {
return getHeader(JoseHeaderNames.X5U);
}
/**
* Returns the X.509 certificate chain that contains the X.509 public key certificate
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE.
* or certificate chain corresponding to the key used to digitally sign the JWS or
* encrypt the JWE. The certificate or certificate chain is represented as a
* {@code List} of certificate value {@code String}s. Each {@code String} in the
* {@code List} is a Base64-encoded DER PKIX certificate value.
*
* @return the X.509 certificate chain
*/
@@ -125,16 +129,6 @@ public final class JoseHeader {
return getHeader(JoseHeaderNames.X5T_S256);
}
/**
* Returns the critical headers that indicates which extensions to the JWS/JWE/JWA specifications
* are being used that MUST be understood and processed.
*
* @return the critical headers
*/
public Set<String> getCritical() {
return getHeader(JoseHeaderNames.CRIT);
}
/**
* Returns the type header that declares the media type of the JWS/JWE.
*
@@ -153,6 +147,16 @@ public final class JoseHeader {
return getHeader(JoseHeaderNames.CTY);
}
/**
* Returns the critical headers that indicates which extensions to the JWS/JWE/JWA specifications
* are being used that MUST be understood and processed.
*
* @return the critical headers
*/
public Set<String> getCritical() {
return getHeader(JoseHeaderNames.CRIT);
}
/**
* Returns the headers.
*
@@ -185,13 +189,13 @@ public final class JoseHeader {
}
/**
* Returns a new {@link Builder}, initialized with the provided {@link JwsAlgorithm}.
* Returns a new {@link Builder}, initialized with the provided {@link JwaAlgorithm}.
*
* @param jwsAlgorithm the {@link JwsAlgorithm}
* @param jwaAlgorithm the {@link JwaAlgorithm}
* @return the {@link Builder}
*/
public static Builder withAlgorithm(JwsAlgorithm jwsAlgorithm) {
return new Builder(jwsAlgorithm);
public static Builder withAlgorithm(JwaAlgorithm jwaAlgorithm) {
return new Builder(jwaAlgorithm);
}
/**
@@ -213,9 +217,8 @@ public final class JoseHeader {
private Builder() {
}
private Builder(JwsAlgorithm jwsAlgorithm) {
Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
header(JoseHeaderNames.ALG, jwsAlgorithm);
private Builder(JwaAlgorithm jwaAlgorithm) {
algorithm(jwaAlgorithm);
}
private Builder(JoseHeader headers) {
@@ -224,24 +227,25 @@ public final class JoseHeader {
}
/**
* Sets the {@link JwsAlgorithm JWS algorithm} used to digitally sign the JWS.
* Sets the {@link JwaAlgorithm JWA algorithm} used to digitally sign the JWS or encrypt the JWE.
*
* @param jwsAlgorithm the JWS algorithm
* @param jwaAlgorithm the {@link JwaAlgorithm}
* @return the {@link Builder}
*/
public Builder jwsAlgorithm(JwsAlgorithm jwsAlgorithm) {
return header(JoseHeaderNames.ALG, jwsAlgorithm);
public Builder algorithm(JwaAlgorithm jwaAlgorithm) {
Assert.notNull(jwaAlgorithm, "jwaAlgorithm cannot be null");
return header(JoseHeaderNames.ALG, jwaAlgorithm);
}
/**
* Sets the JWK Set URL that refers to the resource of a set of JSON-encoded public keys,
* one of which corresponds to the key used to digitally sign the JWS or encrypt the JWE.
*
* @param jwkSetUri the JWK Set URL
* @param jwkSetUrl the JWK Set URL
* @return the {@link Builder}
*/
public Builder jwkSetUri(String jwkSetUri) {
return header(JoseHeaderNames.JKU, jwkSetUri);
public Builder jwkSetUrl(String jwkSetUrl) {
return header(JoseHeaderNames.JKU, convertAsURL(JoseHeaderNames.JKU, jwkSetUrl));
}
/**
@@ -269,16 +273,19 @@ public final class JoseHeader {
* Sets the X.509 URL that refers to the resource for the X.509 public key certificate
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE.
*
* @param x509Uri the X.509 URL
* @param x509Url the X.509 URL
* @return the {@link Builder}
*/
public Builder x509Uri(String x509Uri) {
return header(JoseHeaderNames.X5U, x509Uri);
public Builder x509Url(String x509Url) {
return header(JoseHeaderNames.X5U, convertAsURL(JoseHeaderNames.X5U, x509Url));
}
/**
* Sets the X.509 certificate chain that contains the X.509 public key certificate
* or certificate chain corresponding to the key used to digitally sign the JWS or encrypt the JWE.
* or certificate chain corresponding to the key used to digitally sign the JWS or
* encrypt the JWE. The certificate or certificate chain is represented as a
* {@code List} of certificate value {@code String}s. Each {@code String} in the
* {@code List} is a Base64-encoded DER PKIX certificate value.
*
* @param x509CertificateChain the X.509 certificate chain
* @return the {@link Builder}
@@ -309,17 +316,6 @@ public final class JoseHeader {
return header(JoseHeaderNames.X5T_S256, x509SHA256Thumbprint);
}
/**
* Sets the critical headers that indicates which extensions to the JWS/JWE/JWA specifications
* are being used that MUST be understood and processed.
*
* @param headerNames the critical header names
* @return the {@link Builder}
*/
public Builder critical(Set<String> headerNames) {
return header(JoseHeaderNames.CRIT, headerNames);
}
/**
* Sets the type header that declares the media type of the JWS/JWE.
*
@@ -340,6 +336,17 @@ public final class JoseHeader {
return header(JoseHeaderNames.CTY, contentType);
}
/**
* Sets the critical headers that indicates which extensions to the JWS/JWE/JWA specifications
* are being used that MUST be understood and processed.
*
* @param headerNames the critical header names
* @return the {@link Builder}
*/
public Builder critical(Set<String> headerNames) {
return header(JoseHeaderNames.CRIT, headerNames);
}
/**
* Sets the header.
*
@@ -373,19 +380,15 @@ public final class JoseHeader {
*/
public JoseHeader build() {
Assert.notEmpty(this.headers, "headers cannot be empty");
convertAsURL(JoseHeaderNames.JKU);
convertAsURL(JoseHeaderNames.X5U);
return new JoseHeader(this.headers);
}
private void convertAsURL(String header) {
Object value = this.headers.get(header);
if (value != null) {
URL convertedValue = ClaimConversionService.getSharedInstance().convert(value, URL.class);
Assert.isTrue(convertedValue != null,
() -> "Unable to convert header '" + header + "' of type '" + value.getClass() + "' to URL.");
this.headers.put(header, convertedValue);
}
private static URL convertAsURL(String header, String value) {
URL convertedValue = ClaimConversionService.getSharedInstance().convert(value, URL.class);
Assert.isTrue(convertedValue != null,
() -> "Unable to convert header '" + header + "' of type '" + value.getClass() + "' to URL.");
return convertedValue;
}
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.security.oauth2.jwt;
import java.net.URL;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
@@ -22,6 +23,7 @@ import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
import org.springframework.util.Assert;
/**
@@ -32,7 +34,7 @@ import org.springframework.util.Assert;
* @since 0.0.1
* @see Jwt
* @see JwtClaimAccessor
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4">JWT Claims Set</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7519#section-4">JWT Claims Set</a>
*/
public final class JwtClaimsSet implements JwtClaimAccessor {
private final Map<String, Object> claims;
@@ -166,10 +168,10 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
}
/**
* A {@code Consumer} to be provided access to the claims set
* A {@code Consumer} to be provided access to the claims
* allowing the ability to add, replace, or remove.
*
* @param claimsConsumer a {@code Consumer} of the claims set
* @param claimsConsumer a {@code Consumer} of the claims
*/
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
claimsConsumer.accept(this.claims);
@@ -183,6 +185,17 @@ public final class JwtClaimsSet implements JwtClaimAccessor {
*/
public JwtClaimsSet build() {
Assert.notEmpty(this.claims, "claims cannot be empty");
// The value of the 'iss' claim is a String or URL (StringOrURI).
// Attempt to convert to URL.
Object issuer = this.claims.get(JwtClaimNames.ISS);
if (issuer != null) {
URL convertedValue = ClaimConversionService.getSharedInstance().convert(issuer, URL.class);
if (convertedValue != null) {
this.claims.put(JwtClaimNames.ISS, convertedValue);
}
}
return new JwtClaimsSet(this.claims);
}
}

View File

@@ -32,11 +32,11 @@ package org.springframework.security.oauth2.jwt;
* @see JoseHeader
* @see JwtClaimsSet
* @see JwtDecoder
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516">JSON Web Encryption (JWE)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS Compact Serialization</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7516#section-3.1">JWE Compact Serialization</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7519">JSON Web Token (JWT)</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7515">JSON Web Signature (JWS)</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7516">JSON Web Encryption (JWE)</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7515#section-3.1">JWS Compact Serialization</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7516#section-3.1">JWE Compact Serialization</a>
*/
@FunctionalInterface
public interface JwtEncoder {

View File

@@ -15,26 +15,28 @@
*/
package org.springframework.security.oauth2.jwt;
import java.net.URI;
import java.net.URL;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.KeySourceException;
import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKMatcher;
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.KeyType;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.produce.JWSSignerFactory;
@@ -62,27 +64,17 @@ import org.springframework.util.StringUtils;
* @see JwtEncoder
* @see com.nimbusds.jose.jwk.source.JWKSource
* @see com.nimbusds.jose.jwk.JWK
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token
* (JWT)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature
* (JWS)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515#section-3.1">JWS
* Compact Serialization</a>
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus
* JOSE + JWT SDK</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7519">JSON Web Token (JWT)</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7515">JSON Web Signature (JWS)</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7515#section-3.1">JWS Compact Serialization</a>
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus JOSE + JWT SDK</a>
*/
public final class NimbusJwsEncoder implements JwtEncoder {
private static final String ENCODING_ERROR_MESSAGE_TEMPLATE = "An error occurred while attempting to encode the Jwt: %s";
private static final Converter<JoseHeader, JWSHeader> JWS_HEADER_CONVERTER = new JwsHeaderConverter();
private static final Converter<JwtClaimsSet, JWTClaimsSet> JWT_CLAIMS_SET_CONVERTER = new JwtClaimsSetConverter();
private static final JWSSignerFactory JWS_SIGNER_FACTORY = new DefaultJWSSignerFactory();
private final Map<JWK, JWSSigner> jwsSigners = new ConcurrentHashMap<>();
private final JWKSource<SecurityContext> jwkSource;
/**
@@ -100,108 +92,126 @@ public final class NimbusJwsEncoder implements JwtEncoder {
Assert.notNull(claims, "claims cannot be null");
JWK jwk = selectJwk(headers);
if (jwk == null) {
throw new JwtEncodingException(
String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to select a JWK signing key"));
}
else if (!StringUtils.hasText(jwk.getKeyID())) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"The \"kid\" (key ID) from the selected JWK cannot be empty"));
}
headers = addKeyIdentifierHeadersIfNecessary(headers, jwk);
// @formatter:off
headers = JoseHeader.from(headers)
.type(JOSEObjectType.JWT.getType())
.keyId(jwk.getKeyID())
.build();
claims = JwtClaimsSet.from(claims)
.id(UUID.randomUUID().toString())
.build();
// @formatter:on
JWSHeader jwsHeader = JWS_HEADER_CONVERTER.convert(headers);
JWTClaimsSet jwtClaimsSet = JWT_CLAIMS_SET_CONVERTER.convert(claims);
JWSSigner jwsSigner = this.jwsSigners.computeIfAbsent(jwk, (key) -> {
try {
return JWS_SIGNER_FACTORY.createJWSSigner(key);
}
catch (JOSEException ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to create a JWS Signer -> " + ex.getMessage()), ex);
}
});
SignedJWT signedJwt = new SignedJWT(jwsHeader, jwtClaimsSet);
try {
signedJwt.sign(jwsSigner);
}
catch (JOSEException ex) {
throw new JwtEncodingException(
String.format(ENCODING_ERROR_MESSAGE_TEMPLATE, "Failed to sign the JWT -> " + ex.getMessage()), ex);
}
String jws = signedJwt.serialize();
String jws = serialize(headers, claims, jwk);
return new Jwt(jws, claims.getIssuedAt(), claims.getExpiresAt(), headers.getHeaders(), claims.getClaims());
}
private JWK selectJwk(JoseHeader headers) {
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(headers.getJwsAlgorithm().getName());
JWSHeader jwsHeader = new JWSHeader(jwsAlgorithm);
JWKSelector jwkSelector = new JWKSelector(JWKMatcher.forJWSHeader(jwsHeader));
List<JWK> jwks;
try {
JWKSelector jwkSelector = new JWKSelector(createJwkMatcher(headers));
jwks = this.jwkSource.get(jwkSelector, null);
}
catch (KeySourceException ex) {
} catch (Exception ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to select a JWK signing key -> " + ex.getMessage()), ex);
}
if (jwks.size() > 1) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Found multiple JWK signing keys for algorithm '" + jwsAlgorithm.getName() + "'"));
"Found multiple JWK signing keys for algorithm '" + headers.getAlgorithm().getName() + "'"));
}
return !jwks.isEmpty() ? jwks.get(0) : null;
if (jwks.isEmpty()) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to select a JWK signing key"));
}
return jwks.get(0);
}
private String serialize(JoseHeader headers, JwtClaimsSet claims, JWK jwk) {
JWSHeader jwsHeader = JWS_HEADER_CONVERTER.convert(headers);
JWTClaimsSet jwtClaimsSet = JWT_CLAIMS_SET_CONVERTER.convert(claims);
JWSSigner jwsSigner = this.jwsSigners.computeIfAbsent(jwk, NimbusJwsEncoder::createSigner);
SignedJWT signedJwt = new SignedJWT(jwsHeader, jwtClaimsSet);
try {
signedJwt.sign(jwsSigner);
} catch (JOSEException ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to sign the JWT -> " + ex.getMessage()), ex);
}
return signedJwt.serialize();
}
private static JWKMatcher createJwkMatcher(JoseHeader headers) {
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(headers.getAlgorithm().getName());
if (JWSAlgorithm.Family.RSA.contains(jwsAlgorithm) || JWSAlgorithm.Family.EC.contains(jwsAlgorithm)) {
// @formatter:off
return new JWKMatcher.Builder()
.keyType(KeyType.forAlgorithm(jwsAlgorithm))
.keyID(headers.getKeyId())
.keyUses(KeyUse.SIGNATURE, null)
.algorithms(jwsAlgorithm, null)
.x509CertSHA256Thumbprint(Base64URL.from(headers.getX509SHA256Thumbprint()))
.build();
// @formatter:on
} else if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) {
// @formatter:off
return new JWKMatcher.Builder()
.keyType(KeyType.forAlgorithm(jwsAlgorithm))
.keyID(headers.getKeyId())
.privateOnly(true)
.algorithms(jwsAlgorithm, null)
.build();
// @formatter:on
}
return null;
}
private static JoseHeader addKeyIdentifierHeadersIfNecessary(JoseHeader headers, JWK jwk) {
// Check if headers have already been added
if (StringUtils.hasText(headers.getKeyId()) && StringUtils.hasText(headers.getX509SHA256Thumbprint())) {
return headers;
}
// Check if headers can be added from JWK
if (!StringUtils.hasText(jwk.getKeyID()) && jwk.getX509CertSHA256Thumbprint() == null) {
return headers;
}
JoseHeader.Builder headersBuilder = JoseHeader.from(headers);
if (!StringUtils.hasText(headers.getKeyId()) && StringUtils.hasText(jwk.getKeyID())) {
headersBuilder.keyId(jwk.getKeyID());
}
if (!StringUtils.hasText(headers.getX509SHA256Thumbprint()) && jwk.getX509CertSHA256Thumbprint() != null) {
headersBuilder.x509SHA256Thumbprint(jwk.getX509CertSHA256Thumbprint().toString());
}
return headersBuilder.build();
}
private static JWSSigner createSigner(JWK jwk) {
try {
return JWS_SIGNER_FACTORY.createJWSSigner(jwk);
} catch (JOSEException ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to create a JWS Signer -> " + ex.getMessage()), ex);
}
}
private static class JwsHeaderConverter implements Converter<JoseHeader, JWSHeader> {
@Override
public JWSHeader convert(JoseHeader headers) {
JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(headers.getJwsAlgorithm().getName()));
JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(headers.getAlgorithm().getName()));
Set<String> critical = headers.getCritical();
if (!CollectionUtils.isEmpty(critical)) {
builder.criticalParams(critical);
}
String contentType = headers.getContentType();
if (StringUtils.hasText(contentType)) {
builder.contentType(contentType);
}
URL jwkSetUri = headers.getJwkSetUri();
if (jwkSetUri != null) {
try {
builder.jwkURL(jwkSetUri.toURI());
}
catch (Exception ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to convert '" + JoseHeaderNames.JKU + "' JOSE header to a URI"), ex);
}
if (headers.getJwkSetUrl() != null) {
builder.jwkURL(convertAsURI(JoseHeaderNames.JKU, headers.getJwkSetUrl()));
}
Map<String, Object> jwk = headers.getJwk();
if (!CollectionUtils.isEmpty(jwk)) {
try {
builder.jwk(JWK.parse(jwk));
}
catch (Exception ex) {
} catch (Exception ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to convert '" + JoseHeaderNames.JWK + "' JOSE header"), ex);
"Unable to convert '" + JoseHeaderNames.JWK + "' JOSE header"), ex);
}
}
@@ -210,14 +220,17 @@ public final class NimbusJwsEncoder implements JwtEncoder {
builder.keyID(keyId);
}
String type = headers.getType();
if (StringUtils.hasText(type)) {
builder.type(new JOSEObjectType(type));
if (headers.getX509Url() != null) {
builder.x509CertURL(convertAsURI(JoseHeaderNames.X5U, headers.getX509Url()));
}
List<String> x509CertificateChain = headers.getX509CertificateChain();
if (!CollectionUtils.isEmpty(x509CertificateChain)) {
builder.x509CertChain(x509CertificateChain.stream().map(Base64::new).collect(Collectors.toList()));
List<Base64> x5cList = new ArrayList<>();
x509CertificateChain.forEach((x5c) -> x5cList.add(new Base64(x5c)));
if (!x5cList.isEmpty()) {
builder.x509CertChain(x5cList);
}
}
String x509SHA1Thumbprint = headers.getX509SHA1Thumbprint();
@@ -230,27 +243,43 @@ public final class NimbusJwsEncoder implements JwtEncoder {
builder.x509CertSHA256Thumbprint(new Base64URL(x509SHA256Thumbprint));
}
URL x509Uri = headers.getX509Uri();
if (x509Uri != null) {
try {
builder.x509CertURL(x509Uri.toURI());
}
catch (Exception ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Failed to convert '" + JoseHeaderNames.X5U + "' JOSE header to a URI"), ex);
}
String type = headers.getType();
if (StringUtils.hasText(type)) {
builder.type(new JOSEObjectType(type));
}
Map<String, Object> customHeaders = headers.getHeaders().entrySet().stream()
.filter((header) -> !JWSHeader.getRegisteredParameterNames().contains(header.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (!CollectionUtils.isEmpty(customHeaders)) {
String contentType = headers.getContentType();
if (StringUtils.hasText(contentType)) {
builder.contentType(contentType);
}
Set<String> critical = headers.getCritical();
if (!CollectionUtils.isEmpty(critical)) {
builder.criticalParams(critical);
}
Map<String, Object> customHeaders = new HashMap<>();
headers.getHeaders().forEach((name, value) -> {
if (!JWSHeader.getRegisteredParameterNames().contains(name)) {
customHeaders.put(name, value);
}
});
if (!customHeaders.isEmpty()) {
builder.customParams(customHeaders);
}
return builder.build();
}
private static URI convertAsURI(String header, URL url) {
try {
return url.toURI();
} catch (Exception ex) {
throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
"Unable to convert '" + header + "' JOSE header to a URI"), ex);
}
}
}
private static class JwtClaimsSetConverter implements Converter<JwtClaimsSet, JWTClaimsSet> {
@@ -259,9 +288,10 @@ public final class NimbusJwsEncoder implements JwtEncoder {
public JWTClaimsSet convert(JwtClaimsSet claims) {
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
URL issuer = claims.getIssuer();
// NOTE: The value of the 'iss' claim is a String or URL (StringOrURI).
Object issuer = claims.getClaim(JwtClaimNames.ISS);
if (issuer != null) {
builder.issuer(issuer.toExternalForm());
builder.issuer(issuer.toString());
}
String subject = claims.getSubject();
@@ -274,11 +304,6 @@ public final class NimbusJwsEncoder implements JwtEncoder {
builder.audience(audience);
}
Instant issuedAt = claims.getIssuedAt();
if (issuedAt != null) {
builder.issueTime(Date.from(issuedAt));
}
Instant expiresAt = claims.getExpiresAt();
if (expiresAt != null) {
builder.expirationTime(Date.from(expiresAt));
@@ -289,15 +314,23 @@ public final class NimbusJwsEncoder implements JwtEncoder {
builder.notBeforeTime(Date.from(notBefore));
}
Instant issuedAt = claims.getIssuedAt();
if (issuedAt != null) {
builder.issueTime(Date.from(issuedAt));
}
String jwtId = claims.getId();
if (StringUtils.hasText(jwtId)) {
builder.jwtID(jwtId);
}
Map<String, Object> customClaims = claims.getClaims().entrySet().stream()
.filter((claim) -> !JWTClaimsSet.getRegisteredNames().contains(claim.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (!CollectionUtils.isEmpty(customClaims)) {
Map<String, Object> customClaims = new HashMap<>();
claims.getClaims().forEach((name, value) -> {
if (!JWTClaimsSet.getRegisteredNames().contains(name)) {
customClaims.put(name, value);
}
});
if (!customClaims.isEmpty()) {
customClaims.forEach(builder::claim);
}

View File

@@ -23,6 +23,7 @@ import java.util.concurrent.ConcurrentHashMap;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
@@ -96,10 +97,12 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
@Override
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
Assert.hasText(token, "token cannot be empty");
return this.authorizations.values().stream()
.filter(authorization -> hasToken(authorization, token, tokenType))
.findFirst()
.orElse(null);
for (OAuth2Authorization authorization : this.authorizations.values()) {
if (hasToken(authorization, token, tokenType)) {
return authorization;
}
}
return null;
}
private static boolean hasToken(OAuth2Authorization authorization, String token, @Nullable OAuth2TokenType tokenType) {

View File

@@ -47,8 +47,8 @@ import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
@@ -253,9 +253,12 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
}
private OAuth2Authorization findBy(String filter, List<SqlParameterValue> parameters) {
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
List<OAuth2Authorization> result = this.jdbcOperations.query(LOAD_AUTHORIZATION_SQL + filter, pss, this.authorizationRowMapper);
return !result.isEmpty() ? result.get(0) : null;
try (LobCreator lobCreator = getLobHandler().getLobCreator()) {
PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator,
parameters.toArray());
List<OAuth2Authorization> result = getJdbcOperations().query(LOAD_AUTHORIZATION_SQL + filter, pss, getAuthorizationRowMapper());
return !result.isEmpty() ? result.get(0) : null;
}
}
/**
@@ -405,7 +408,7 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
}
Map<String, Object> refreshTokenMetadata = parseMap(rs.getString("refresh_token_metadata"));
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken2(
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(
tokenValue, tokenIssuedAt, tokenExpiresAt);
builder.token(refreshToken, (metadata) -> metadata.putAll(refreshTokenMetadata));
}

View File

@@ -15,52 +15,80 @@
*/
package org.springframework.security.oauth2.server.authorization;
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.context.Context;
import org.springframework.security.oauth2.jwt.JoseHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.util.Assert;
/**
* An {@link OAuth2TokenContext} implementation used when encoding a {@link Jwt}.
*
* @author Joe Grandja
* @since 0.1.0
* @see OAuth2TokenContext
* @see JoseHeader.Builder
* @see JwtClaimsSet.Builder
* @see JwtEncoder#encode(JoseHeader, JwtClaimsSet)
*/
public final class JwtEncodingContext implements OAuth2TokenContext {
private final Context context;
private final Map<Object, Object> context;
private JwtEncodingContext(Map<Object, Object> context) {
this.context = Context.of(context);
this.context = Collections.unmodifiableMap(new HashMap<>(context));
}
@SuppressWarnings("unchecked")
@Nullable
@Override
public <V> V get(Object key) {
return this.context.get(key);
return hasKey(key) ? (V) this.context.get(key) : null;
}
@Override
public boolean hasKey(Object key) {
return this.context.hasKey(key);
Assert.notNull(key, "key cannot be null");
return this.context.containsKey(key);
}
/**
* Returns the {@link JoseHeader.Builder headers}.
*
* @return the {@link JoseHeader.Builder}
*/
public JoseHeader.Builder getHeaders() {
return get(JoseHeader.Builder.class);
}
/**
* Returns the {@link JwtClaimsSet.Builder claims}.
*
* @return the {@link JwtClaimsSet.Builder}
*/
public JwtClaimsSet.Builder getClaims() {
return get(JwtClaimsSet.Builder.class);
}
/**
* Constructs a new {@link Builder} with the provided headers and claims.
*
* @param headersBuilder the headers to initialize the builder
* @param claimsBuilder the claims to initialize the builder
* @return the {@link Builder}
*/
public static Builder with(JoseHeader.Builder headersBuilder, JwtClaimsSet.Builder claimsBuilder) {
return new Builder(headersBuilder, claimsBuilder);
}
/**
* A builder for {@link JwtEncodingContext}.
*/
public static final class Builder extends AbstractBuilder<JwtEncodingContext, Builder> {
private Builder(JoseHeader.Builder headersBuilder, JwtClaimsSet.Builder claimsBuilder) {
@@ -70,18 +98,39 @@ public final class JwtEncodingContext implements OAuth2TokenContext {
put(JwtClaimsSet.Builder.class, claimsBuilder);
}
/**
* A {@code Consumer} of the {@link JoseHeader.Builder headers}
* allowing the ability to add, replace, or remove.
*
* @param headersConsumer a {@code Consumer} of the {@link JoseHeader.Builder headers}
* @return the {@link Builder} for further configuration
*/
public Builder headers(Consumer<JoseHeader.Builder> headersConsumer) {
headersConsumer.accept(get(JoseHeader.Builder.class));
return this;
}
/**
* A {@code Consumer} of the {@link JwtClaimsSet.Builder claims}
* allowing the ability to add, replace, or remove.
*
* @param claimsConsumer a {@code Consumer} of the {@link JwtClaimsSet.Builder claims}
* @return the {@link Builder} for further configuration
*/
public Builder claims(Consumer<JwtClaimsSet.Builder> claimsConsumer) {
claimsConsumer.accept(get(JwtClaimsSet.Builder.class));
return this;
}
/**
* Builds a new {@link JwtEncodingContext}.
*
* @return the {@link JwtEncodingContext}
*/
public JwtEncodingContext build() {
return new JwtEncodingContext(this.context);
return new JwtEncodingContext(getContext());
}
}
}

View File

@@ -25,12 +25,11 @@ import java.util.UUID;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.Version;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.Version;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -46,7 +45,7 @@ import org.springframework.util.StringUtils;
* @since 0.0.1
* @see RegisteredClient
* @see AuthorizationGrantType
* @see AbstractOAuth2Token
* @see OAuth2Token
* @see OAuth2AccessToken
* @see OAuth2RefreshToken
*/
@@ -64,7 +63,7 @@ public class OAuth2Authorization implements Serializable {
private String registeredClientId;
private String principalName;
private AuthorizationGrantType authorizationGrantType;
private Map<Class<? extends AbstractOAuth2Token>, Token<?>> tokens;
private Map<Class<? extends OAuth2Token>, Token<?>> tokens;
private Map<String, Object> attributes;
protected OAuth2Authorization() {
@@ -134,7 +133,7 @@ public class OAuth2Authorization implements Serializable {
*/
@Nullable
@SuppressWarnings("unchecked")
public <T extends AbstractOAuth2Token> Token<T> getToken(Class<T> tokenType) {
public <T extends OAuth2Token> Token<T> getToken(Class<T> tokenType) {
Assert.notNull(tokenType, "tokenType cannot be null");
Token<?> token = this.tokens.get(tokenType);
return token != null ? (Token<T>) token : null;
@@ -149,13 +148,14 @@ public class OAuth2Authorization implements Serializable {
*/
@Nullable
@SuppressWarnings("unchecked")
public <T extends AbstractOAuth2Token> Token<T> getToken(String tokenValue) {
public <T extends OAuth2Token> Token<T> getToken(String tokenValue) {
Assert.hasText(tokenValue, "tokenValue cannot be empty");
Token<?> token = this.tokens.values().stream()
.filter(t -> t.getToken().getTokenValue().equals(tokenValue))
.findFirst()
.orElse(null);
return token != null ? (Token<T>) token : null;
for (Token<?> token : this.tokens.values()) {
if (token.getToken().getTokenValue().equals(tokenValue)) {
return (Token<T>) token;
}
}
return null;
}
/**
@@ -237,19 +237,19 @@ public class OAuth2Authorization implements Serializable {
* @author Joe Grandja
* @since 0.1.0
*/
public static class Token<T extends AbstractOAuth2Token> implements Serializable {
public static class Token<T extends OAuth2Token> implements Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
protected static final String TOKEN_METADATA_BASE = "metadata.token.";
protected static final String TOKEN_METADATA_NAMESPACE = "metadata.token.";
/**
* The name of the metadata that indicates if the token has been invalidated.
*/
public static final String INVALIDATED_METADATA_NAME = TOKEN_METADATA_BASE.concat("invalidated");
public static final String INVALIDATED_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("invalidated");
/**
* The name of the metadata used for the claims of the token.
*/
public static final String CLAIMS_METADATA_NAME = TOKEN_METADATA_BASE.concat("claims");
public static final String CLAIMS_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("claims");
private final T token;
private final Map<String, Object> metadata;
@@ -264,9 +264,9 @@ public class OAuth2Authorization implements Serializable {
}
/**
* Returns the token of type {@link AbstractOAuth2Token}.
* Returns the token of type {@link OAuth2Token}.
*
* @return the token of type {@link AbstractOAuth2Token}
* @return the token of type {@link OAuth2Token}
*/
public T getToken() {
return this.token;
@@ -380,7 +380,7 @@ public class OAuth2Authorization implements Serializable {
private final String registeredClientId;
private String principalName;
private AuthorizationGrantType authorizationGrantType;
private Map<Class<? extends AbstractOAuth2Token>, Token<?>> tokens = new HashMap<>();
private Map<Class<? extends OAuth2Token>, Token<?>> tokens = new HashMap<>();
private final Map<String, Object> attributes = new HashMap<>();
protected Builder(String registeredClientId) {
@@ -441,25 +441,25 @@ public class OAuth2Authorization implements Serializable {
}
/**
* Sets the {@link AbstractOAuth2Token token}.
* Sets the {@link OAuth2Token token}.
*
* @param token the token
* @param <T> the type of the token
* @return the {@link Builder}
*/
public <T extends AbstractOAuth2Token> Builder token(T token) {
public <T extends OAuth2Token> Builder token(T token) {
return token(token, (metadata) -> {});
}
/**
* Sets the {@link AbstractOAuth2Token token} and associated metadata.
* Sets the {@link OAuth2Token token} and associated metadata.
*
* @param token the token
* @param metadataConsumer a {@code Consumer} of the metadata {@code Map}
* @param <T> the type of the token
* @return the {@link Builder}
*/
public <T extends AbstractOAuth2Token> Builder token(T token,
public <T extends OAuth2Token> Builder token(T token,
Consumer<Map<String, Object>> metadataConsumer) {
Assert.notNull(token, "token cannot be null");
@@ -469,15 +469,12 @@ public class OAuth2Authorization implements Serializable {
metadata.putAll(existingToken.getMetadata());
}
metadataConsumer.accept(metadata);
Class<? extends AbstractOAuth2Token> tokenClass = token.getClass();
if (tokenClass.equals(OAuth2RefreshToken2.class)) {
tokenClass = OAuth2RefreshToken.class;
}
Class<? extends OAuth2Token> tokenClass = token.getClass();
this.tokens.put(tokenClass, new Token<>(token, metadata));
return this;
}
protected final Builder tokens(Map<Class<? extends AbstractOAuth2Token>, Token<?>> tokens) {
protected final Builder tokens(Map<Class<? extends OAuth2Token>, Token<?>> tokens) {
this.tokens = new HashMap<>(tokens);
return this;
}
@@ -529,5 +526,7 @@ public class OAuth2Authorization implements Serializable {
authorization.attributes = Collections.unmodifiableMap(this.attributes);
return authorization;
}
}
}

View File

@@ -21,7 +21,6 @@ import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.springframework.lang.NonNull;
import org.springframework.security.core.GrantedAuthority;
@@ -91,11 +90,13 @@ public final class OAuth2AuthorizationConsent implements Serializable {
* @return the {@code scope}s granted to the client by the principal.
*/
public Set<String> getScopes() {
return getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.filter(authority -> authority.startsWith(AUTHORITIES_SCOPE_PREFIX))
.map(scope -> scope.replaceFirst(AUTHORITIES_SCOPE_PREFIX, ""))
.collect(Collectors.toSet());
Set<String> authorities = new HashSet<>();
for (GrantedAuthority authority : getAuthorities()) {
if (authority.getAuthority().startsWith(AUTHORITIES_SCOPE_PREFIX)) {
authorities.add(authority.getAuthority().replaceFirst(AUTHORITIES_SCOPE_PREFIX, ""));
}
}
return authorities;
}
@Override

View File

@@ -30,78 +30,174 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
import org.springframework.util.Assert;
/**
* A context that holds information associated to an OAuth 2.0 Token
* and is used by an {@link OAuth2TokenCustomizer} for customizing the token attributes.
*
* @author Joe Grandja
* @since 0.1.0
* @see Context
* @see OAuth2TokenCustomizer
*/
public interface OAuth2TokenContext extends Context {
/**
* Returns the {@link RegisteredClient registered client}.
*
* @return the {@link RegisteredClient}
*/
default RegisteredClient getRegisteredClient() {
return get(RegisteredClient.class);
}
/**
* Returns the {@link Authentication} representing the {@code Principal} resource owner (or client).
*
* @param <T> the type of the {@code Authentication}
* @return the {@link Authentication} representing the {@code Principal} resource owner (or client)
*/
default <T extends Authentication> T getPrincipal() {
return get(AbstractBuilder.PRINCIPAL_AUTHENTICATION_KEY);
}
/**
* Returns the {@link OAuth2Authorization authorization}.
*
* @return the {@link OAuth2Authorization}, or {@code null} if not available
*/
@Nullable
default OAuth2Authorization getAuthorization() {
return get(OAuth2Authorization.class);
}
/**
* Returns the authorized scope(s).
*
* @return the authorized scope(s)
*/
default Set<String> getAuthorizedScopes() {
return hasKey(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME) ?
get(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME) :
Collections.emptySet();
}
/**
* Returns the {@link OAuth2TokenType token type}.
*
* @return the {@link OAuth2TokenType}
*/
default OAuth2TokenType getTokenType() {
return get(OAuth2TokenType.class);
}
/**
* Returns the {@link AuthorizationGrantType authorization grant type}.
*
* @return the {@link AuthorizationGrantType}
*/
default AuthorizationGrantType getAuthorizationGrantType() {
return get(AuthorizationGrantType.class);
}
/**
* Returns the {@link Authentication} representing the authorization grant.
*
* @param <T> the type of the {@code Authentication}
* @return the {@link Authentication} representing the authorization grant
*/
default <T extends Authentication> T getAuthorizationGrant() {
return get(AbstractBuilder.AUTHORIZATION_GRANT_AUTHENTICATION_KEY);
}
/**
* Base builder for implementations of {@link OAuth2TokenContext}.
*
* @param <T> the type of the context
* @param <B> the type of the builder
*/
abstract class AbstractBuilder<T extends OAuth2TokenContext, B extends AbstractBuilder<T, B>> {
private static final String PRINCIPAL_AUTHENTICATION_KEY =
Authentication.class.getName().concat(".PRINCIPAL");
private static final String AUTHORIZATION_GRANT_AUTHENTICATION_KEY =
Authentication.class.getName().concat(".AUTHORIZATION_GRANT");
protected final Map<Object, Object> context = new HashMap<>();
private final Map<Object, Object> context = new HashMap<>();
/**
* Sets the {@link RegisteredClient registered client}.
*
* @param registeredClient the {@link RegisteredClient}
* @return the {@link AbstractBuilder} for further configuration
*/
public B registeredClient(RegisteredClient registeredClient) {
return put(RegisteredClient.class, registeredClient);
}
/**
* Sets the {@link Authentication} representing the {@code Principal} resource owner (or client).
*
* @param principal the {@link Authentication} representing the {@code Principal} resource owner (or client)
* @return the {@link AbstractBuilder} for further configuration
*/
public B principal(Authentication principal) {
return put(PRINCIPAL_AUTHENTICATION_KEY, principal);
}
/**
* Sets the {@link OAuth2Authorization authorization}.
*
* @param authorization the {@link OAuth2Authorization}
* @return the {@link AbstractBuilder} for further configuration
*/
public B authorization(OAuth2Authorization authorization) {
return put(OAuth2Authorization.class, authorization);
}
/**
* Sets the authorized scope(s).
*
* @param authorizedScopes the authorized scope(s)
* @return the {@link AbstractBuilder} for further configuration
*/
public B authorizedScopes(Set<String> authorizedScopes) {
return put(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
}
/**
* Sets the {@link OAuth2TokenType token type}.
*
* @param tokenType the {@link OAuth2TokenType}
* @return the {@link AbstractBuilder} for further configuration
*/
public B tokenType(OAuth2TokenType tokenType) {
return put(OAuth2TokenType.class, tokenType);
}
/**
* Sets the {@link AuthorizationGrantType authorization grant type}.
*
* @param authorizationGrantType the {@link AuthorizationGrantType}
* @return the {@link AbstractBuilder} for further configuration
*/
public B authorizationGrantType(AuthorizationGrantType authorizationGrantType) {
return put(AuthorizationGrantType.class, authorizationGrantType);
}
/**
* Sets the {@link Authentication} representing the authorization grant.
*
* @param authorizationGrant the {@link Authentication} representing the authorization grant
* @return the {@link AbstractBuilder} for further configuration
*/
public B authorizationGrant(Authentication authorizationGrant) {
return put(AUTHORIZATION_GRANT_AUTHENTICATION_KEY, authorizationGrant);
}
/**
* Associates an attribute.
*
* @param key the key for the attribute
* @param value the value of the attribute
* @return the {@link AbstractBuilder} for further configuration
*/
public B put(Object key, Object value) {
Assert.notNull(key, "key cannot be null");
Assert.notNull(value, "value cannot be null");
@@ -109,6 +205,13 @@ public interface OAuth2TokenContext extends Context {
return getThis();
}
/**
* A {@code Consumer} of the attributes {@code Map}
* allowing the ability to add, replace, or remove.
*
* @param contextConsumer a {@link Consumer} of the attributes {@code Map}
* @return the {@link AbstractBuilder} for further configuration
*/
public B context(Consumer<Map<Object, Object>> contextConsumer) {
contextConsumer.accept(this.context);
return getThis();
@@ -119,12 +222,22 @@ public interface OAuth2TokenContext extends Context {
return (V) this.context.get(key);
}
protected Map<Object, Object> getContext() {
return this.context;
}
@SuppressWarnings("unchecked")
protected B getThis() {
protected final B getThis() {
return (B) this;
}
/**
* Builds a new {@link OAuth2TokenContext}.
*
* @return the {@link OAuth2TokenContext}
*/
public abstract T build();
}
}

View File

@@ -16,13 +16,22 @@
package org.springframework.security.oauth2.server.authorization;
/**
* Implementations of this interface are responsible for customizing the
* OAuth 2.0 Token attributes contained within the {@link OAuth2TokenContext}.
*
* @author Joe Grandja
* @since 0.1.0
* @see OAuth2TokenContext
* @param <T> the type of the context containing the OAuth 2.0 Token attributes
*/
@FunctionalInterface
public interface OAuth2TokenCustomizer<C extends OAuth2TokenContext> {
public interface OAuth2TokenCustomizer<T extends OAuth2TokenContext> {
void customize(C context);
/**
* Customize the OAuth 2.0 Token attributes.
*
* @param context the context containing the OAuth 2.0 Token attributes
*/
void customize(T context);
}

View File

@@ -50,7 +50,7 @@ final class JwtUtils {
String issuer, String subject, Set<String> authorizedScopes) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().accessTokenTimeToLive());
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
// @formatter:off
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder();

View File

@@ -19,11 +19,10 @@ import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
/**
* Utility methods for the OAuth 2.0 {@link AuthenticationProvider}'s.
@@ -44,7 +43,7 @@ final class OAuth2AuthenticationProviderUtils {
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
return clientPrincipal;
}
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
static <T extends AbstractOAuth2Token> OAuth2Authorization invalidate(

View File

@@ -16,19 +16,27 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2TokenType;
@@ -43,7 +51,6 @@ import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
@@ -69,14 +76,17 @@ import static org.springframework.security.oauth2.server.authorization.authentic
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
*/
public class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
public final class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE =
new OAuth2TokenType(OAuth2ParameterNames.CODE);
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE =
new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private static final StringKeyGenerator DEFAULT_REFRESH_TOKEN_GENERATOR =
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
private final OAuth2AuthorizationService authorizationService;
private final JwtEncoder jwtEncoder;
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
private Supplier<String> refreshTokenGenerator = DEFAULT_REFRESH_TOKEN_GENERATOR::generateKey;
private ProviderSettings providerSettings;
/**
@@ -92,12 +102,29 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
this.jwtEncoder = jwtEncoder;
}
public final void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
/**
* Sets the {@link OAuth2TokenCustomizer} that customizes the
* {@link JwtEncodingContext.Builder#headers(Consumer) headers} and/or
* {@link JwtEncodingContext.Builder#claims(Consumer) claims} for the generated {@link Jwt}.
*
* @param jwtCustomizer the {@link OAuth2TokenCustomizer} that customizes the headers and/or claims for the generated {@code Jwt}
*/
public void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
this.jwtCustomizer = jwtCustomizer;
}
@Autowired(required = false)
/**
* Sets the {@code Supplier<String>} that generates the value for the {@link OAuth2RefreshToken}.
*
* @param refreshTokenGenerator the {@code Supplier<String>} that generates the value for the {@link OAuth2RefreshToken}
*/
public void setRefreshTokenGenerator(Supplier<String> refreshTokenGenerator) {
Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null");
this.refreshTokenGenerator = refreshTokenGenerator;
}
@Autowired
protected void setProviderSettings(ProviderSettings providerSettings) {
this.providerSettings = providerSettings;
}
@@ -114,7 +141,7 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
OAuth2Authorization authorization = this.authorizationService.findByToken(
authorizationCodeAuthentication.getCode(), AUTHORIZATION_CODE_TOKEN_TYPE);
if (authorization == null) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
authorization.getToken(OAuth2AuthorizationCode.class);
@@ -128,19 +155,19 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken());
this.authorizationService.save(authorization);
}
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
if (StringUtils.hasText(authorizationRequest.getRedirectUri()) &&
!authorizationRequest.getRedirectUri().equals(authorizationCodeAuthentication.getRedirectUri())) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
if (!authorizationCode.isActive()) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null;
String issuer = this.providerSettings != null ? this.providerSettings.getIssuer() : null;
Set<String> authorizedScopes = authorization.getAttribute(
OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME);
@@ -172,9 +199,10 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
jwtAccessToken.getExpiresAt(), authorizedScopes);
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
refreshToken = OAuth2RefreshTokenAuthenticationProvider.generateRefreshToken(
registeredClient.getTokenSettings().refreshTokenTimeToLive());
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// Do not issue refresh token to public client
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
refreshToken = generateRefreshToken(registeredClient.getTokenSettings().getRefreshTokenTimeToLive());
}
Jwt jwtIdToken = null;
@@ -250,4 +278,10 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
private OAuth2RefreshToken generateRefreshToken(Duration tokenTimeToLive) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(tokenTimeToLive);
return new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt);
}
}

View File

@@ -20,26 +20,33 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit;
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.regex.Pattern;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
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;
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationContext;
import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationValidator;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
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;
@@ -55,6 +62,7 @@ import org.springframework.web.util.UriComponentsBuilder;
* used in the Authorization Code Grant.
*
* @author Joe Grandja
* @author Steve Riesenberg
* @since 0.1.2
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
* @see OAuth2AuthorizationCodeAuthenticationProvider
@@ -63,16 +71,21 @@ import org.springframework.web.util.UriComponentsBuilder;
* @see OAuth2AuthorizationConsentService
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
*/
public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
private static final Pattern LOOPBACK_ADDRESS_PATTERN =
Pattern.compile("^127(?:\\.[0-9]+){0,2}\\.[0-9]+$|^\\[(?:0*:)*?:?0*1]$");
private static final StringKeyGenerator DEFAULT_AUTHORIZATION_CODE_GENERATOR =
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
new Base64StringKeyGenerator(Base64.getUrlEncoder());
private static final Function<String, OAuth2AuthenticationValidator> DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER =
createDefaultAuthenticationValidatorResolver();
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2AuthorizationConsentService authorizationConsentService;
private final StringKeyGenerator codeGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
private Supplier<String> authorizationCodeGenerator = DEFAULT_AUTHORIZATION_CODE_GENERATOR::generateKey;
private Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER;
private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
/**
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using the provided parameters.
@@ -106,6 +119,61 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* Sets the {@code Supplier<String>} that generates the value for the {@link OAuth2AuthorizationCode}.
*
* @param authorizationCodeGenerator the {@code Supplier<String>} that generates the value for the {@link OAuth2AuthorizationCode}
*/
public void setAuthorizationCodeGenerator(Supplier<String> 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;
@@ -117,29 +185,21 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
authorizationCodeRequestAuthentication, null);
}
if (StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
if (!isValidRedirectUri(authorizationCodeRequestAuthentication.getRedirectUri(), registeredClient)) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
authorizationCodeRequestAuthentication, registeredClient);
}
} else 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);
}
Map<Object, Object> context = new HashMap<>();
context.put(RegisteredClient.class, registeredClient);
OAuth2AuthenticationContext authenticationContext = new OAuth2AuthenticationContext(
authorizationCodeRequestAuthentication, context);
OAuth2AuthenticationValidator redirectUriValidator = resolveAuthenticationValidator(OAuth2ParameterNames.REDIRECT_URI);
redirectUriValidator.validate(authenticationContext);
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
authorizationCodeRequestAuthentication, registeredClient);
}
Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
Set<String> allowedScopes = registeredClient.getScopes();
if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
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);
@@ -151,7 +211,7 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
authorizationCodeRequestAuthentication, registeredClient, null);
}
}
} else if (registeredClient.getClientSettings().requireProofKey()) {
} else if (registeredClient.getClientSettings().isRequireProofKey()) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
authorizationCodeRequestAuthentication, registeredClient, null);
}
@@ -170,7 +230,7 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
.clientId(registeredClient.getClientId())
.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
.scopes(requestedScopes)
.scopes(authorizationCodeRequestAuthentication.getScopes())
.state(authorizationCodeRequestAuthentication.getState())
.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
.build();
@@ -179,7 +239,7 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
registeredClient.getId(), principal.getName());
if (requireAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {
String state = this.stateGenerator.generateKey();
String state = DEFAULT_STATE_GENERATOR.generateKey();
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
.attribute(OAuth2ParameterNames.STATE, state)
.build();
@@ -198,21 +258,13 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
.build();
}
OAuth2AuthorizationCode authorizationCode = createAuthorizationCode();
OAuth2AuthorizationCode authorizationCode = generateAuthorizationCode();
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
.token(authorizationCode)
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes())
.build();
this.authorizationService.save(authorization);
// TODO security checks for code parameter
// The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks.
// A maximum authorization code lifetime of 10 minutes is RECOMMENDED.
// The client MUST NOT use the authorization code more than once.
// If an authorization code is used more than once, the authorization server MUST deny the request
// and SHOULD revoke (when possible) all tokens previously issued based on that authorization code.
// The authorization code is bound to the client identifier and redirection URI.
String redirectUri = authorizationRequest.getRedirectUri();
if (!StringUtils.hasText(redirectUri)) {
redirectUri = registeredClient.getRedirectUris().iterator().next();
@@ -227,10 +279,17 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
.build();
}
private OAuth2AuthorizationCode createAuthorizationCode() {
private OAuth2AuthenticationValidator resolveAuthenticationValidator(String parameterName) {
OAuth2AuthenticationValidator authenticationValidator = this.authenticationValidatorResolver.apply(parameterName);
return authenticationValidator != null ?
authenticationValidator :
DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER.apply(parameterName);
}
private OAuth2AuthorizationCode generateAuthorizationCode() {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
return new OAuth2AuthorizationCode(this.codeGenerator.generateKey(), issuedAt, expiresAt);
return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.get(), issuedAt, expiresAt);
}
private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException {
@@ -271,18 +330,6 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
currentAuthorizationConsent.getScopes() : Collections.emptySet();
if (authorizedScopes.isEmpty() && currentAuthorizedScopes.isEmpty()) {
// Authorization consent denied
this.authorizationService.remove(authorization);
throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
}
if (requestedScopes.contains(OidcScopes.OPENID)) {
// 'openid' scope is auto-approved as it does not require consent
authorizedScopes.add(OidcScopes.OPENID);
}
if (!currentAuthorizedScopes.isEmpty()) {
for (String requestedScope : requestedScopes) {
if (currentAuthorizedScopes.contains(requestedScope)) {
@@ -291,20 +338,52 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
}
}
if (!authorizedScopes.isEmpty() && !authorizedScopes.equals(currentAuthorizedScopes)) {
OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
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) {
authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
} else {
authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
authorization.getRegisteredClientId(), authorization.getPrincipalName());
this.authorizationConsentService.remove(currentAuthorizationConsent);
}
authorizedScopes.forEach(authorizationConsentBuilder::scope);
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
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);
}
OAuth2AuthorizationCode authorizationCode = createAuthorizationCode();
OAuth2AuthorizationCode authorizationCode = generateAuthorizationCode();
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
.token(authorizationCode)
@@ -329,6 +408,13 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
.build();
}
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;
}
private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal,
OAuth2AuthorizationRequest authorizationRequest) {
return OAuth2Authorization.withRegisteredClient(registeredClient)
@@ -341,7 +427,7 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
private static boolean requireAuthorizationConsent(RegisteredClient registeredClient,
OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {
if (!registeredClient.getClientSettings().requireUserConsent()) {
if (!registeredClient.getClientSettings().isRequireAuthorizationConsent()) {
return false;
}
// 'openid' scope does not require consent
@@ -377,7 +463,7 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
// redirects described in Section 10.3.3, the use of "localhost" is NOT RECOMMENDED.
return false;
}
if (!LOOPBACK_ADDRESS_PATTERN.matcher(requestedRedirectHost).matches()) {
if (!isLoopbackAddress(requestedRedirectHost)) {
// As per https://tools.ietf.org/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.
@@ -399,6 +485,28 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
return false;
}
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 boolean isPrincipalAuthenticated(Authentication principal) {
return principal != null &&
!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
@@ -474,4 +582,45 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements Aut
.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
}
private static class DefaultRedirectUriOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
@Override
public void validate(OAuth2AuthenticationContext authenticationContext) {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
authenticationContext.getAuthentication();
RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class);
if (StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
if (!isValidRedirectUri(authorizationCodeRequestAuthentication.getRedirectUri(), registeredClient)) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
authorizationCodeRequestAuthentication, registeredClient);
}
} else 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 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);
}
}
}
}

View File

@@ -27,7 +27,7 @@ import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.Version;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2020-2021 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.Map;
import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationContext;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.util.Assert;
/**
* An {@link OAuth2AuthenticationContext} that holds an {@link OAuth2AuthorizationConsent.Builder} and additional information
* and is used when customizing the building of the {@link OAuth2AuthorizationConsent}.
*
* @author Steve Riesenberg
* @author Joe Grandja
* @since 0.2.1
* @see OAuth2AuthenticationContext
* @see OAuth2AuthorizationConsent
*/
public final class OAuth2AuthorizationConsentAuthenticationContext extends OAuth2AuthenticationContext {
private OAuth2AuthorizationConsentAuthenticationContext(Map<Object, Object> context) {
super(context);
}
/**
* Returns the {@link OAuth2AuthorizationConsent.Builder authorization consent builder}.
*
* @return the {@link OAuth2AuthorizationConsent.Builder}
*/
public OAuth2AuthorizationConsent.Builder getAuthorizationConsent() {
return get(OAuth2AuthorizationConsent.Builder.class);
}
/**
* Returns the {@link RegisteredClient registered client}.
*
* @return the {@link RegisteredClient}
*/
public RegisteredClient getRegisteredClient() {
return get(RegisteredClient.class);
}
/**
* Returns the {@link OAuth2Authorization authorization}.
*
* @return the {@link OAuth2Authorization}
*/
public OAuth2Authorization getAuthorization() {
return get(OAuth2Authorization.class);
}
/**
* Returns the {@link OAuth2AuthorizationRequest authorization request}.
*
* @return the {@link OAuth2AuthorizationRequest}
*/
public OAuth2AuthorizationRequest getAuthorizationRequest() {
return get(OAuth2AuthorizationRequest.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 OAuth2AuthorizationConsentAuthenticationContext}.
*/
public static final class Builder extends AbstractBuilder<OAuth2AuthorizationConsentAuthenticationContext, Builder> {
private Builder(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
super(authentication);
}
/**
* Sets the {@link OAuth2AuthorizationConsent.Builder authorization consent builder}.
*
* @param authorizationConsent the {@link OAuth2AuthorizationConsent.Builder}
* @return the {@link Builder} for further configuration
*/
public Builder authorizationConsent(OAuth2AuthorizationConsent.Builder authorizationConsent) {
return put(OAuth2AuthorizationConsent.Builder.class, authorizationConsent);
}
/**
* 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);
}
/**
* Sets the {@link OAuth2Authorization authorization}.
*
* @param authorization the {@link OAuth2Authorization}
* @return the {@link Builder} for further configuration
*/
public Builder authorization(OAuth2Authorization authorization) {
return put(OAuth2Authorization.class, authorization);
}
/**
* Sets the {@link OAuth2AuthorizationRequest authorization request}.
*
* @param authorizationRequest the {@link OAuth2AuthorizationRequest}
* @return the {@link Builder} for further configuration
*/
public Builder authorizationRequest(OAuth2AuthorizationRequest authorizationRequest) {
return put(OAuth2AuthorizationRequest.class, authorizationRequest);
}
/**
* Builds a new {@link OAuth2AuthorizationConsentAuthenticationContext}.
*
* @return the {@link OAuth2AuthorizationConsentAuthenticationContext}
*/
public OAuth2AuthorizationConsentAuthenticationContext build() {
Assert.notNull(get(OAuth2AuthorizationConsent.Builder.class), "authorizationConsentBuilder cannot be null");
Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null");
Assert.notNull(get(OAuth2Authorization.class), "authorization cannot be null");
Assert.notNull(get(OAuth2AuthorizationRequest.class), "authorizationRequest cannot be null");
return new OAuth2AuthorizationConsentAuthenticationContext(getContext());
}
}
}

View File

@@ -39,7 +39,6 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -55,7 +54,8 @@ import org.springframework.util.StringUtils;
* @see OAuth2AuthorizationService
* @see PasswordEncoder
*/
public class OAuth2ClientAuthenticationProvider implements AuthenticationProvider {
public final class OAuth2ClientAuthenticationProvider implements AuthenticationProvider {
private static final String CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-3.2.1";
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
@@ -84,7 +84,7 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
*
* @param passwordEncoder the {@link PasswordEncoder} used to validate the client secret
*/
public final void setPasswordEncoder(PasswordEncoder passwordEncoder) {
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
}
@@ -97,31 +97,32 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
String clientId = clientAuthentication.getPrincipal().toString();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
throwInvalidClient();
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
}
if (!registeredClient.getClientAuthenticationMethods().contains(
clientAuthentication.getClientAuthenticationMethod())) {
throwInvalidClient();
throwInvalidClient("authentication_method");
}
boolean authenticatedCredentials = false;
boolean credentialsAuthenticated = false;
if (clientAuthentication.getCredentials() != null) {
String clientSecret = clientAuthentication.getCredentials().toString();
if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {
throwInvalidClient();
throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
}
authenticatedCredentials = true;
credentialsAuthenticated = true;
}
authenticatedCredentials = authenticatedCredentials ||
authenticatePkceIfAvailable(clientAuthentication, registeredClient);
if (!authenticatedCredentials) {
throwInvalidClient();
boolean pkceAuthenticated = authenticatePkceIfAvailable(clientAuthentication, registeredClient);
credentialsAuthenticated = credentialsAuthenticated || pkceAuthenticated;
if (!credentialsAuthenticated) {
throwInvalidClient("credentials");
}
return new OAuth2ClientAuthenticationToken(registeredClient);
return new OAuth2ClientAuthenticationToken(registeredClient,
clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
}
@Override
@@ -133,7 +134,7 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
RegisteredClient registeredClient) {
Map<String, Object> parameters = clientAuthentication.getAdditionalParameters();
if (CollectionUtils.isEmpty(parameters) || !authorizationCodeGrant(parameters)) {
if (!authorizationCodeGrant(parameters)) {
return false;
}
@@ -141,7 +142,7 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
(String) parameters.get(OAuth2ParameterNames.CODE),
AUTHORIZATION_CODE_TOKEN_TYPE);
if (authorization == null) {
throwInvalidClient();
throwInvalidClient(OAuth2ParameterNames.CODE);
}
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
@@ -149,16 +150,19 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
String codeChallenge = (String) authorizationRequest.getAdditionalParameters()
.get(PkceParameterNames.CODE_CHALLENGE);
if (!StringUtils.hasText(codeChallenge) &&
registeredClient.getClientSettings().requireProofKey()) {
throwInvalidClient();
if (!StringUtils.hasText(codeChallenge)) {
if (registeredClient.getClientSettings().isRequireProofKey()) {
throwInvalidClient(PkceParameterNames.CODE_CHALLENGE);
} else {
return false;
}
}
String codeChallengeMethod = (String) authorizationRequest.getAdditionalParameters()
.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
String codeVerifier = (String) parameters.get(PkceParameterNames.CODE_VERIFIER);
if (!codeVerifierValid(codeVerifier, codeChallenge, codeChallengeMethod)) {
throwInvalidClient();
throwInvalidClient(PkceParameterNames.CODE_VERIFIER);
}
return true;
@@ -174,7 +178,7 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
if (!StringUtils.hasText(codeVerifier)) {
return false;
} else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) {
return codeVerifier.equals(codeChallenge);
return codeVerifier.equals(codeChallenge);
} else if ("S256".equals(codeChallengeMethod)) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
@@ -186,10 +190,15 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
// there will likely be bigger issues as well. We default to SERVER_ERROR.
}
}
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.SERVER_ERROR);
}
private static void throwInvalidClient() {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
private static void throwInvalidClient(String parameterName) {
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_CLIENT,
"Client authentication failed: " + parameterName,
CLIENT_AUTHENTICATION_ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 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,17 +15,18 @@
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.Collections;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.Transient;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.Version;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.Map;
/**
* An {@link Authentication} implementation used for OAuth 2.0 Client Authentication.
*
@@ -37,96 +38,93 @@ import java.util.Map;
* @see RegisteredClient
* @see OAuth2ClientAuthenticationProvider
*/
@Transient
public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private String clientId;
private String clientSecret;
private ClientAuthenticationMethod clientAuthenticationMethod;
private Map<String, Object> additionalParameters;
private RegisteredClient registeredClient;
private final String clientId;
private final RegisteredClient registeredClient;
private final ClientAuthenticationMethod clientAuthenticationMethod;
private final Object credentials;
private final Map<String, Object> additionalParameters;
/**
* Constructs an {@code OAuth2ClientAuthenticationToken} using the provided parameters.
*
* @param clientId the client identifier
* @param clientSecret the client secret
* @param clientAuthenticationMethod the authentication method used by the client
* @param credentials the client credentials
* @param additionalParameters the additional parameters
*/
public OAuth2ClientAuthenticationToken(String clientId, String clientSecret,
ClientAuthenticationMethod clientAuthenticationMethod,
@Nullable Map<String, Object> additionalParameters) {
this(clientId, additionalParameters);
Assert.hasText(clientSecret, "clientSecret cannot be empty");
Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
this.clientSecret = clientSecret;
this.clientAuthenticationMethod = clientAuthenticationMethod;
}
/**
* Constructs an {@code OAuth2ClientAuthenticationToken} using the provided parameters.
*
* @param clientId the client identifier
* @param additionalParameters the additional parameters
*/
public OAuth2ClientAuthenticationToken(String clientId,
@Nullable Map<String, Object> additionalParameters) {
public OAuth2ClientAuthenticationToken(String clientId, ClientAuthenticationMethod clientAuthenticationMethod,
@Nullable Object credentials, @Nullable Map<String, Object> additionalParameters) {
super(Collections.emptyList());
Assert.hasText(clientId, "clientId cannot be empty");
Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
this.clientId = clientId;
this.additionalParameters = additionalParameters != null ?
Collections.unmodifiableMap(additionalParameters) : null;
this.clientAuthenticationMethod = ClientAuthenticationMethod.NONE;
this.registeredClient = null;
this.clientAuthenticationMethod = clientAuthenticationMethod;
this.credentials = credentials;
this.additionalParameters = Collections.unmodifiableMap(
additionalParameters != null ? additionalParameters : Collections.emptyMap());
}
/**
* Constructs an {@code OAuth2ClientAuthenticationToken} using the provided parameters.
*
* @param registeredClient the registered client
* @param registeredClient the authenticated registered client
* @param clientAuthenticationMethod the authentication method used by the client
* @param credentials the client credentials
*/
public OAuth2ClientAuthenticationToken(RegisteredClient registeredClient) {
public OAuth2ClientAuthenticationToken(RegisteredClient registeredClient, ClientAuthenticationMethod clientAuthenticationMethod,
@Nullable Object credentials) {
super(Collections.emptyList());
Assert.notNull(registeredClient, "registeredClient cannot be null");
Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
this.clientId = registeredClient.getClientId();
this.registeredClient = registeredClient;
this.clientAuthenticationMethod = clientAuthenticationMethod;
this.credentials = credentials;
this.additionalParameters = Collections.unmodifiableMap(Collections.emptyMap());
setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return this.registeredClient != null ?
this.registeredClient.getClientId() :
this.clientId;
return this.clientId;
}
@Nullable
@Override
public Object getCredentials() {
return this.clientSecret;
return this.credentials;
}
/**
* Returns the additional parameters
* Returns the authenticated {@link RegisteredClient registered client}, or {@code null} if not authenticated.
*
* @return the additional parameters
* @return the authenticated {@link RegisteredClient}, or {@code null} if not authenticated
*/
public @Nullable Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
/**
* Returns the {@link RegisteredClient registered client}.
*
* @return the {@link RegisteredClient}
*/
public @Nullable RegisteredClient getRegisteredClient() {
@Nullable
public RegisteredClient getRegisteredClient() {
return this.registeredClient;
}
/**
* Returns the {@link ClientAuthenticationMethod client authentication method}.
* Returns the {@link ClientAuthenticationMethod authentication method} used by the client.
*
* @return the {@link ClientAuthenticationMethod}
* @return the {@link ClientAuthenticationMethod} used by the client
*/
public @Nullable ClientAuthenticationMethod getClientAuthenticationMethod() {
public ClientAuthenticationMethod getClientAuthenticationMethod() {
return this.clientAuthenticationMethod;
}
/**
* Returns the additional parameters.
*
* @return the additional parameters
*/
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
}

View File

@@ -17,7 +17,7 @@ package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.function.Consumer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
@@ -26,19 +26,18 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.jwt.JoseHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -59,7 +58,7 @@ import static org.springframework.security.oauth2.server.authorization.authentic
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.4">Section 4.4 Client Credentials Grant</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.4.2">Section 4.4.2 Access Token Request</a>
*/
public class OAuth2ClientCredentialsAuthenticationProvider implements AuthenticationProvider {
public final class OAuth2ClientCredentialsAuthenticationProvider implements AuthenticationProvider {
private final OAuth2AuthorizationService authorizationService;
private final JwtEncoder jwtEncoder;
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
@@ -79,12 +78,19 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica
this.jwtEncoder = jwtEncoder;
}
public final void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
/**
* Sets the {@link OAuth2TokenCustomizer} that customizes the
* {@link JwtEncodingContext.Builder#headers(Consumer) headers} and/or
* {@link JwtEncodingContext.Builder#claims(Consumer) claims} for the generated {@link Jwt}.
*
* @param jwtCustomizer the {@link OAuth2TokenCustomizer} that customizes the headers and/or claims for the generated {@code Jwt}
*/
public void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
this.jwtCustomizer = jwtCustomizer;
}
@Autowired(required = false)
@Autowired
protected void setProviderSettings(ProviderSettings providerSettings) {
this.providerSettings = providerSettings;
}
@@ -99,21 +105,20 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.CLIENT_CREDENTIALS)) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
Set<String> authorizedScopes = registeredClient.getScopes(); // Default to configured scopes
if (!CollectionUtils.isEmpty(clientCredentialsAuthentication.getScopes())) {
Set<String> unauthorizedScopes = clientCredentialsAuthentication.getScopes().stream()
.filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope))
.collect(Collectors.toSet());
if (!CollectionUtils.isEmpty(unauthorizedScopes)) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE));
for (String requestedScope : clientCredentialsAuthentication.getScopes()) {
if (!registeredClient.getScopes().contains(requestedScope)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
}
authorizedScopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
}
String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null;
String issuer = this.providerSettings != null ? this.providerSettings.getIssuer() : null;
JoseHeader.Builder headersBuilder = JwtUtils.headers();
JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(

View File

@@ -23,6 +23,8 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
@@ -33,10 +35,8 @@ import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
@@ -72,12 +72,14 @@ import static org.springframework.security.oauth2.server.authorization.authentic
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.5">Section 1.5 Refresh Token Grant</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a>
*/
public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
public final class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private static final StringKeyGenerator TOKEN_GENERATOR = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
private static final StringKeyGenerator DEFAULT_REFRESH_TOKEN_GENERATOR =
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
private final OAuth2AuthorizationService authorizationService;
private final JwtEncoder jwtEncoder;
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
private Supplier<String> refreshTokenGenerator = DEFAULT_REFRESH_TOKEN_GENERATOR::generateKey;
private ProviderSettings providerSettings;
/**
@@ -94,12 +96,29 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
this.jwtEncoder = jwtEncoder;
}
public final void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
/**
* Sets the {@link OAuth2TokenCustomizer} that customizes the
* {@link JwtEncodingContext.Builder#headers(Consumer) headers} and/or
* {@link JwtEncodingContext.Builder#claims(Consumer) claims} for the generated {@link Jwt}.
*
* @param jwtCustomizer the {@link OAuth2TokenCustomizer} that customizes the headers and/or claims for the generated {@code Jwt}
*/
public void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
this.jwtCustomizer = jwtCustomizer;
}
@Autowired(required = false)
/**
* Sets the {@code Supplier<String>} that generates the value for the {@link OAuth2RefreshToken}.
*
* @param refreshTokenGenerator the {@code Supplier<String>} that generates the value for the {@link OAuth2RefreshToken}
*/
public void setRefreshTokenGenerator(Supplier<String> refreshTokenGenerator) {
Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null");
this.refreshTokenGenerator = refreshTokenGenerator;
}
@Autowired
protected void setProviderSettings(ProviderSettings providerSettings) {
this.providerSettings = providerSettings;
}
@@ -116,15 +135,15 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
OAuth2Authorization authorization = this.authorizationService.findByToken(
refreshTokenAuthentication.getRefreshToken(), OAuth2TokenType.REFRESH_TOKEN);
if (authorization == null) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = authorization.getRefreshToken();
@@ -132,7 +151,7 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
// As per https://tools.ietf.org/html/rfc6749#section-5.2
// invalid_grant: The provided authorization grant (e.g., authorization code,
// resource owner credentials) or refresh token is invalid, expired, revoked [...].
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
// As per https://tools.ietf.org/html/rfc6749#section-6
@@ -141,13 +160,13 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
Set<String> scopes = refreshTokenAuthentication.getScopes();
Set<String> authorizedScopes = authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME);
if (!authorizedScopes.containsAll(scopes)) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
if (scopes.isEmpty()) {
scopes = authorizedScopes;
}
String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null;
String issuer = this.providerSettings != null ? this.providerSettings.getIssuer() : null;
JoseHeader.Builder headersBuilder = JwtUtils.headers();
JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(
@@ -178,8 +197,8 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
TokenSettings tokenSettings = registeredClient.getTokenSettings();
OAuth2RefreshToken currentRefreshToken = refreshToken.getToken();
if (!tokenSettings.reuseRefreshTokens()) {
currentRefreshToken = generateRefreshToken(tokenSettings.refreshTokenTimeToLive());
if (!tokenSettings.isReuseRefreshTokens()) {
currentRefreshToken = generateRefreshToken(tokenSettings.getRefreshTokenTimeToLive());
}
Jwt jwtIdToken = null;
@@ -218,8 +237,10 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
// @formatter:off
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization)
.token(accessToken,
(metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, jwtAccessToken.getClaims()))
(metadata) -> {
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, jwtAccessToken.getClaims());
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false);
})
.refreshToken(currentRefreshToken);
if (idToken != null) {
authorizationBuilder
@@ -247,9 +268,9 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);
}
static OAuth2RefreshToken generateRefreshToken(Duration tokenTimeToLive) {
private OAuth2RefreshToken generateRefreshToken(Duration tokenTimeToLive) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(tokenTimeToLive);
return new OAuth2RefreshToken2(TOKEN_GENERATOR.generateKey(), issuedAt, expiresAt);
return new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt);
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.net.URL;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@@ -47,7 +48,7 @@ import static org.springframework.security.oauth2.server.authorization.authentic
* @see OAuth2AuthorizationService
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.1">Section 2.1 Introspection Request</a>
*/
public class OAuth2TokenIntrospectionAuthenticationProvider implements AuthenticationProvider {
public final class OAuth2TokenIntrospectionAuthenticationProvider implements AuthenticationProvider {
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
@@ -134,7 +135,10 @@ public class OAuth2TokenIntrospectionAuthenticationProvider implements Authentic
if (!CollectionUtils.isEmpty(audience)) {
tokenClaims.audiences(audiences -> audiences.addAll(audience));
}
tokenClaims.issuer(jwtClaims.getIssuer().toExternalForm());
URL issuer = jwtClaims.getIssuer();
if (issuer != null) {
tokenClaims.issuer(issuer.toExternalForm());
}
String jti = jwtClaims.getId();
if (StringUtils.hasText(jti)) {
tokenClaims.id(jti);

View File

@@ -20,7 +20,6 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
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.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -39,7 +38,7 @@ import static org.springframework.security.oauth2.server.authorization.authentic
* @see OAuth2AuthorizationService
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7009#section-2.1">Section 2.1 Revocation Request</a>
*/
public class OAuth2TokenRevocationAuthenticationProvider implements AuthenticationProvider {
public final class OAuth2TokenRevocationAuthenticationProvider implements AuthenticationProvider {
private final OAuth2AuthorizationService authorizationService;
/**
@@ -69,7 +68,7 @@ public class OAuth2TokenRevocationAuthenticationProvider implements Authenticati
}
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
OAuth2Authorization.Token<AbstractOAuth2Token> token = authorization.getToken(tokenRevocationAuthentication.getToken());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 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,15 +15,15 @@
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.Collections;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.Version;
import org.springframework.util.Assert;
import java.util.Collections;
/**
* An {@link Authentication} implementation used for OAuth 2.0 Token Revocation.
*
@@ -62,7 +62,7 @@ public class OAuth2TokenRevocationAuthenticationToken extends AbstractAuthentica
* @param revokedToken the revoked token
* @param clientPrincipal the authenticated client principal
*/
public OAuth2TokenRevocationAuthenticationToken(AbstractOAuth2Token revokedToken,
public OAuth2TokenRevocationAuthenticationToken(OAuth2Token revokedToken,
Authentication clientPrincipal) {
super(Collections.emptyList());
Assert.notNull(revokedToken, "revokedToken cannot be null");

View File

@@ -36,9 +36,12 @@ import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SqlParameterValue;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -54,6 +57,7 @@ import org.springframework.util.StringUtils;
*
* @author Rafal Lewczuk
* @author Joe Grandja
* @author Ovidiu Popa
* @since 0.1.2
* @see RegisteredClientRepository
* @see RegisteredClient
@@ -79,6 +83,8 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
private static final String TABLE_NAME = "oauth2_registered_client";
private static final String PK_FILTER = "id = ?";
private static final String LOAD_REGISTERED_CLIENT_SQL = "SELECT " + COLUMN_NAMES + " FROM " + TABLE_NAME + " WHERE ";
// @formatter:off
@@ -86,6 +92,13 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
+ "(" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
// @formatter:on
// @formatter:off
private static final String UPDATE_REGISTERED_CLIENT_SQL = "UPDATE " + TABLE_NAME
+ " SET client_name = ?, client_authentication_methods = ?, authorization_grant_types = ?,"
+ " redirect_uris = ?, scopes = ?, client_settings = ?, token_settings = ?"
+ " WHERE " + PK_FILTER;
// @formatter:on
private final JdbcOperations jdbcOperations;
private RowMapper<RegisteredClient> registeredClientRowMapper;
private Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper;
@@ -105,14 +118,28 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
@Override
public void save(RegisteredClient registeredClient) {
Assert.notNull(registeredClient, "registeredClient cannot be null");
RegisteredClient existingRegisteredClient = findBy("id = ? OR client_id = ?",
registeredClient.getId(), registeredClient.getClientId());
RegisteredClient existingRegisteredClient = findBy(PK_FILTER,
registeredClient.getId());
if (existingRegisteredClient != null) {
Assert.isTrue(!existingRegisteredClient.getId().equals(registeredClient.getId()),
"Registered client must be unique. Found duplicate identifier: " + registeredClient.getId());
Assert.isTrue(!existingRegisteredClient.getClientId().equals(registeredClient.getClientId()),
"Registered client must be unique. Found duplicate client identifier: " + registeredClient.getClientId());
updateRegisteredClient(registeredClient);
} else {
insertRegisteredClient(registeredClient);
}
}
private void updateRegisteredClient(RegisteredClient registeredClient) {
List<SqlParameterValue> parameters = new ArrayList<>(this.registeredClientParametersMapper.apply(registeredClient));
SqlParameterValue id = parameters.remove(0);
parameters.remove(0); // remove client_id
parameters.remove(0); // remove client_id_issued_at
parameters.remove(0); // remove client_secret
parameters.remove(0); // remove client_secret_expires_at
parameters.add(id);
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
this.jdbcOperations.update(UPDATE_REGISTERED_CLIENT_SQL, pss);
}
private void insertRegisteredClient(RegisteredClient registeredClient) {
List<SqlParameterValue> parameters = this.registeredClientParametersMapper.apply(registeredClient);
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
this.jdbcOperations.update(INSERT_REGISTERED_CLIENT_SQL, pss);
@@ -211,12 +238,10 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
// @formatter:on
Map<String, Object> clientSettingsMap = parseMap(rs.getString("client_settings"));
builder.clientSettings(clientSettings ->
clientSettings.settings().putAll(clientSettingsMap));
builder.clientSettings(ClientSettings.withSettings(clientSettingsMap).build());
Map<String, Object> tokenSettingsMap = parseMap(rs.getString("token_settings"));
builder.tokenSettings(tokenSettings ->
tokenSettings.settings().putAll(tokenSettingsMap));
builder.tokenSettings(TokenSettings.withSettings(tokenSettingsMap).build());
return builder.build();
}
@@ -303,8 +328,8 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(authorizationGrantTypes)),
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(registeredClient.getRedirectUris())),
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(registeredClient.getScopes())),
new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getClientSettings().settings())),
new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getTokenSettings().settings())));
new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getClientSettings().getSettings())),
new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getTokenSettings().getSettings())));
}
public final void setObjectMapper(ObjectMapper objectMapper) {
@@ -312,6 +337,13 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
this.objectMapper = objectMapper;
}
/**
* @deprecated See javadoc {@link RegisteredClientRepository#save(RegisteredClient)}
*/
@Deprecated
public final void setPasswordEncoder(PasswordEncoder passwordEncoder) {
}
protected final ObjectMapper getObjectMapper() {
return this.objectMapper;
}

View File

@@ -90,10 +90,11 @@ public class RegisteredClient implements Serializable {
}
/**
* Returns the client secret.
* Returns the client secret or {@code null} if not available.
*
* @return the client secret
* @return the client secret or {@code null} if not available
*/
@Nullable
public String getClientSecret() {
return this.clientSecret;
}
@@ -190,15 +191,15 @@ public class RegisteredClient implements Serializable {
Objects.equals(this.authorizationGrantTypes, that.authorizationGrantTypes) &&
Objects.equals(this.redirectUris, that.redirectUris) &&
Objects.equals(this.scopes, that.scopes) &&
Objects.equals(this.clientSettings.settings(), that.getClientSettings().settings()) &&
Objects.equals(this.tokenSettings.settings(), that.tokenSettings.settings());
Objects.equals(this.clientSettings, that.clientSettings) &&
Objects.equals(this.tokenSettings, that.tokenSettings);
}
@Override
public int hashCode() {
return Objects.hash(this.id, this.clientId, this.clientIdIssuedAt, this.clientSecret, this.clientSecretExpiresAt,
this.clientName, this.clientAuthenticationMethods, this.authorizationGrantTypes, this.redirectUris,
this.scopes, this.clientSettings.settings(), this.tokenSettings.settings());
this.scopes, this.clientSettings, this.tokenSettings);
}
@Override
@@ -211,8 +212,8 @@ public class RegisteredClient implements Serializable {
", authorizationGrantTypes=" + this.authorizationGrantTypes +
", redirectUris=" + this.redirectUris +
", scopes=" + this.scopes +
", clientSettings=" + this.clientSettings.settings() +
", tokenSettings=" + this.tokenSettings.settings() +
", clientSettings=" + this.clientSettings +
", tokenSettings=" + this.tokenSettings +
'}';
}
@@ -253,34 +254,34 @@ public class RegisteredClient implements Serializable {
private Set<AuthorizationGrantType> authorizationGrantTypes = new HashSet<>();
private Set<String> redirectUris = new HashSet<>();
private Set<String> scopes = new HashSet<>();
private ClientSettings clientSettings = new ClientSettings();
private TokenSettings tokenSettings = new TokenSettings();
private ClientSettings clientSettings;
private TokenSettings tokenSettings;
protected Builder(String id) {
this.id = id;
}
protected Builder(RegisteredClient registeredClient) {
this.id = registeredClient.id;
this.clientId = registeredClient.clientId;
this.clientIdIssuedAt = registeredClient.clientIdIssuedAt;
this.clientSecret = registeredClient.clientSecret;
this.clientSecretExpiresAt = registeredClient.clientSecretExpiresAt;
this.clientName = registeredClient.clientName;
if (!CollectionUtils.isEmpty(registeredClient.clientAuthenticationMethods)) {
this.clientAuthenticationMethods.addAll(registeredClient.clientAuthenticationMethods);
this.id = registeredClient.getId();
this.clientId = registeredClient.getClientId();
this.clientIdIssuedAt = registeredClient.getClientIdIssuedAt();
this.clientSecret = registeredClient.getClientSecret();
this.clientSecretExpiresAt = registeredClient.getClientSecretExpiresAt();
this.clientName = registeredClient.getClientName();
if (!CollectionUtils.isEmpty(registeredClient.getClientAuthenticationMethods())) {
this.clientAuthenticationMethods.addAll(registeredClient.getClientAuthenticationMethods());
}
if (!CollectionUtils.isEmpty(registeredClient.authorizationGrantTypes)) {
this.authorizationGrantTypes.addAll(registeredClient.authorizationGrantTypes);
if (!CollectionUtils.isEmpty(registeredClient.getAuthorizationGrantTypes())) {
this.authorizationGrantTypes.addAll(registeredClient.getAuthorizationGrantTypes());
}
if (!CollectionUtils.isEmpty(registeredClient.redirectUris)) {
this.redirectUris.addAll(registeredClient.redirectUris);
if (!CollectionUtils.isEmpty(registeredClient.getRedirectUris())) {
this.redirectUris.addAll(registeredClient.getRedirectUris());
}
if (!CollectionUtils.isEmpty(registeredClient.scopes)) {
this.scopes.addAll(registeredClient.scopes);
if (!CollectionUtils.isEmpty(registeredClient.getScopes())) {
this.scopes.addAll(registeredClient.getScopes());
}
this.clientSettings = new ClientSettings(registeredClient.clientSettings.settings());
this.tokenSettings = new TokenSettings(registeredClient.tokenSettings.settings());
this.clientSettings = ClientSettings.withSettings(registeredClient.getClientSettings().getSettings()).build();
this.tokenSettings = TokenSettings.withSettings(registeredClient.getTokenSettings().getSettings()).build();
}
/**
@@ -444,26 +445,24 @@ public class RegisteredClient implements Serializable {
}
/**
* A {@link Consumer} of the client configuration settings,
* allowing the ability to add, replace, or remove.
* Sets the {@link ClientSettings client configuration settings}.
*
* @param clientSettingsConsumer a {@link Consumer} of the client configuration settings
* @param clientSettings the client configuration settings
* @return the {@link Builder}
*/
public Builder clientSettings(Consumer<ClientSettings> clientSettingsConsumer) {
clientSettingsConsumer.accept(this.clientSettings);
public Builder clientSettings(ClientSettings clientSettings) {
this.clientSettings = clientSettings;
return this;
}
/**
* A {@link Consumer} of the token configuration settings,
* allowing the ability to add, replace, or remove.
* Sets the {@link TokenSettings token configuration settings}.
*
* @param tokenSettingsConsumer a {@link Consumer} of the token configuration settings
* @param tokenSettings the token configuration settings
* @return the {@link Builder}
*/
public Builder tokenSettings(Consumer<TokenSettings> tokenSettingsConsumer) {
tokenSettingsConsumer.accept(this.tokenSettings);
public Builder tokenSettings(TokenSettings tokenSettings) {
this.tokenSettings = tokenSettings;
return this;
}
@@ -482,7 +481,7 @@ public class RegisteredClient implements Serializable {
this.clientName = this.id;
}
if (CollectionUtils.isEmpty(this.clientAuthenticationMethods)) {
this.clientAuthenticationMethods.add(ClientAuthenticationMethod.BASIC);
this.clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
validateScopes();
validateRedirectUris();
@@ -506,8 +505,10 @@ public class RegisteredClient implements Serializable {
new HashSet<>(this.redirectUris));
registeredClient.scopes = Collections.unmodifiableSet(
new HashSet<>(this.scopes));
registeredClient.clientSettings = new ClientSettings(this.clientSettings.settings());
registeredClient.tokenSettings = new TokenSettings(this.tokenSettings.settings());
registeredClient.clientSettings = this.clientSettings != null ?
this.clientSettings : ClientSettings.builder().build();
registeredClient.tokenSettings = this.tokenSettings != null ?
this.tokenSettings : TokenSettings.builder().build();
return registeredClient;
}

View File

@@ -31,6 +31,9 @@ public interface RegisteredClientRepository {
/**
* Saves the registered client.
*
* <p>
* IMPORTANT: Sensitive information should be encoded externally from the implementation, e.g. {@link RegisteredClient#getClientSecret()}
*
* @param registeredClient the {@link RegisteredClient}
*/
void save(RegisteredClient registeredClient);

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2020-2021 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;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import org.springframework.security.oauth2.core.Version;
import org.springframework.util.Assert;
/**
* Base implementation for configuration settings.
*
* @author Joe Grandja
* @since 0.0.2
*/
public abstract class AbstractSettings implements Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private final Map<String, Object> settings;
protected AbstractSettings(Map<String, Object> settings) {
Assert.notEmpty(settings, "settings cannot be empty");
this.settings = Collections.unmodifiableMap(new HashMap<>(settings));
}
/**
* Returns a configuration setting.
*
* @param name the name of the setting
* @param <T> the type of the setting
* @return the value of the setting, or {@code null} if not available
*/
@SuppressWarnings("unchecked")
public <T> T getSetting(String name) {
Assert.hasText(name, "name cannot be empty");
return (T) getSettings().get(name);
}
/**
* Returns a {@code Map} of the configuration settings.
*
* @return a {@code Map} of the configuration settings
*/
public Map<String, Object> getSettings() {
return this.settings;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
AbstractSettings that = (AbstractSettings) obj;
return this.settings.equals(that.settings);
}
@Override
public int hashCode() {
return Objects.hash(this.settings);
}
@Override
public String toString() {
return "AbstractSettings {" +
"settings=" + this.settings +
'}';
}
/**
* A builder for subclasses of {@link AbstractSettings}.
*/
protected static abstract class AbstractBuilder<T extends AbstractSettings, B extends AbstractBuilder<T, B>> {
private final Map<String, Object> settings = new HashMap<>();
protected AbstractBuilder() {
}
/**
* Sets a configuration setting.
*
* @param name the name of the setting
* @param value the value of the setting
* @return the {@link AbstractBuilder} for further configuration
*/
public B setting(String name, Object value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
getSettings().put(name, value);
return getThis();
}
/**
* A {@code Consumer} of the configuration settings {@code Map}
* allowing the ability to add, replace, or remove.
*
* @param settingsConsumer a {@link Consumer} of the configuration settings {@code Map}
* @return the {@link AbstractBuilder} for further configuration
*/
public B settings(Consumer<Map<String, Object>> settingsConsumer) {
settingsConsumer.accept(getSettings());
return getThis();
}
public abstract T build();
protected final Map<String, Object> getSettings() {
return this.settings;
}
@SuppressWarnings("unchecked")
protected final B getThis() {
return (B) this;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 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,34 +15,21 @@
*/
package org.springframework.security.oauth2.server.authorization.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.Assert;
/**
* A facility for client configuration settings.
*
* @author Joe Grandja
* @since 0.0.2
* @see Settings
* @see AbstractSettings
* @see ConfigurationSettingNames.Client
*/
public class ClientSettings extends Settings {
private static final String CLIENT_SETTING_BASE = "setting.client.";
public static final String REQUIRE_PROOF_KEY = CLIENT_SETTING_BASE.concat("require-proof-key");
public static final String REQUIRE_USER_CONSENT = CLIENT_SETTING_BASE.concat("require-user-consent");
public final class ClientSettings extends AbstractSettings {
/**
* Constructs a {@code ClientSettings}.
*/
public ClientSettings() {
this(defaultSettings());
}
/**
* Constructs a {@code ClientSettings} using the provided parameters.
*
* @param settings the initial settings
*/
public ClientSettings(Map<String, Object> settings) {
private ClientSettings(Map<String, Object> settings) {
super(settings);
}
@@ -52,48 +39,83 @@ public class ClientSettings extends Settings {
*
* @return {@code true} if the client is required to provide a proof key challenge and verifier, {@code false} otherwise
*/
public boolean requireProofKey() {
return setting(REQUIRE_PROOF_KEY);
public boolean isRequireProofKey() {
return getSetting(ConfigurationSettingNames.Client.REQUIRE_PROOF_KEY);
}
/**
* Set to {@code true} if the client is required to provide a proof key challenge and verifier
* when performing the Authorization Code Grant flow.
*
* @param requireProofKey {@code true} if the client is required to provide a proof key challenge and verifier, {@code false} otherwise
* @return the {@link ClientSettings}
*/
public ClientSettings requireProofKey(boolean requireProofKey) {
setting(REQUIRE_PROOF_KEY, requireProofKey);
return this;
}
/**
* Returns {@code true} if the user's consent is required when the client requests access.
* Returns {@code true} if authorization consent is required when the client requests access.
* The default is {@code false}.
*
* @return {@code true} if the user's consent is required when the client requests access, {@code false} otherwise
* @return {@code true} if authorization consent is required when the client requests access, {@code false} otherwise
*/
public boolean requireUserConsent() {
return setting(REQUIRE_USER_CONSENT);
public boolean isRequireAuthorizationConsent() {
return getSetting(ConfigurationSettingNames.Client.REQUIRE_AUTHORIZATION_CONSENT);
}
/**
* Set to {@code true} if the user's consent is required when the client requests access.
* This applies to all interactive flows (e.g. {@code authorization_code} and {@code device_code}).
* Constructs a new {@link Builder} with the default settings.
*
* @param requireUserConsent {@code true} if the user's consent is required when the client requests access, {@code false} otherwise
* @return the {@link ClientSettings}
* @return the {@link Builder}
*/
public ClientSettings requireUserConsent(boolean requireUserConsent) {
setting(REQUIRE_USER_CONSENT, requireUserConsent);
return this;
public static Builder builder() {
return new Builder()
.requireProofKey(false)
.requireAuthorizationConsent(false);
}
protected static Map<String, Object> defaultSettings() {
Map<String, Object> settings = new HashMap<>();
settings.put(REQUIRE_PROOF_KEY, false);
settings.put(REQUIRE_USER_CONSENT, false);
return settings;
/**
* Constructs a new {@link Builder} with the provided settings.
*
* @param settings the settings to initialize the builder
* @return the {@link Builder}
*/
public static Builder withSettings(Map<String, Object> settings) {
Assert.notEmpty(settings, "settings cannot be empty");
return new Builder()
.settings(s -> s.putAll(settings));
}
/**
* A builder for {@link ClientSettings}.
*/
public static class Builder extends AbstractBuilder<ClientSettings, Builder> {
private Builder() {
}
/**
* Set to {@code true} if the client is required to provide a proof key challenge and verifier
* when performing the Authorization Code Grant flow.
*
* @param requireProofKey {@code true} if the client is required to provide a proof key challenge and verifier, {@code false} otherwise
* @return the {@link Builder} for further configuration
*/
public Builder requireProofKey(boolean requireProofKey) {
return setting(ConfigurationSettingNames.Client.REQUIRE_PROOF_KEY, requireProofKey);
}
/**
* Set to {@code true} if authorization consent is required when the client requests access.
* This applies to all interactive flows (e.g. {@code authorization_code} and {@code device_code}).
*
* @param requireAuthorizationConsent {@code true} if authorization consent is required when the client requests access, {@code false} otherwise
* @return the {@link Builder} for further configuration
*/
public Builder requireAuthorizationConsent(boolean requireAuthorizationConsent) {
return setting(ConfigurationSettingNames.Client.REQUIRE_AUTHORIZATION_CONSENT, requireAuthorizationConsent);
}
/**
* Builds the {@link ClientSettings}.
*
* @return the {@link ClientSettings}
*/
@Override
public ClientSettings build() {
return new ClientSettings(getSettings());
}
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2020-2021 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;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
/**
* The names for all the configuration settings.
*
* @author Joe Grandja
* @since 0.2.0
*/
public final class ConfigurationSettingNames {
private static final String SETTINGS_NAMESPACE = "settings.";
private ConfigurationSettingNames() {
}
/**
* The names for client configuration settings.
*/
public static final class Client {
private static final String CLIENT_SETTINGS_NAMESPACE = SETTINGS_NAMESPACE.concat("client.");
/**
* Set to {@code true} if the client is required to provide a proof key challenge and verifier
* when performing the Authorization Code Grant flow.
*/
public static final String REQUIRE_PROOF_KEY = CLIENT_SETTINGS_NAMESPACE.concat("require-proof-key");
/**
* Set to {@code true} if authorization consent is required when the client requests access.
* This applies to all interactive flows (e.g. {@code authorization_code} and {@code device_code}).
*/
public static final String REQUIRE_AUTHORIZATION_CONSENT = CLIENT_SETTINGS_NAMESPACE.concat("require-authorization-consent");
private Client() {
}
}
/**
* The names for provider configuration settings.
*/
public static final class Provider {
private static final String PROVIDER_SETTINGS_NAMESPACE = SETTINGS_NAMESPACE.concat("provider.");
/**
* Set the URL the Provider uses as its Issuer Identifier.
*/
public static final String ISSUER = PROVIDER_SETTINGS_NAMESPACE.concat("issuer");
/**
* Set the Provider's OAuth 2.0 Authorization endpoint.
*/
public static final String AUTHORIZATION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("authorization-endpoint");
/**
* Set the Provider's OAuth 2.0 Token endpoint.
*/
public static final String TOKEN_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("token-endpoint");
/**
* Set the Provider's JWK Set endpoint.
*/
public static final String JWK_SET_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("jwk-set-endpoint");
/**
* Set the Provider's OAuth 2.0 Token Revocation endpoint.
*/
public static final String TOKEN_REVOCATION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("token-revocation-endpoint");
/**
* Set the Provider's OAuth 2.0 Token Introspection endpoint.
*/
public static final String TOKEN_INTROSPECTION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("token-introspection-endpoint");
/**
* Set the Provider's OpenID Connect 1.0 Client Registration endpoint.
*/
public static final String OIDC_CLIENT_REGISTRATION_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("oidc-client-registration-endpoint");
/**
* Set the Provider's OpenID Connect 1.0 UserInfo endpoint.
*/
public static final String OIDC_USER_INFO_ENDPOINT = PROVIDER_SETTINGS_NAMESPACE.concat("oidc-user-info-endpoint");
private Provider() {
}
}
/**
* The names for token configuration settings.
*/
public static final class Token {
private static final String TOKEN_SETTINGS_NAMESPACE = SETTINGS_NAMESPACE.concat("token.");
/**
* Set the time-to-live for an access token.
*/
public static final String ACCESS_TOKEN_TIME_TO_LIVE = TOKEN_SETTINGS_NAMESPACE.concat("access-token-time-to-live");
/**
* Set to {@code true} if refresh tokens are reused when returning the access token response,
* or {@code false} if a new refresh token is issued.
*/
public static final String REUSE_REFRESH_TOKENS = TOKEN_SETTINGS_NAMESPACE.concat("reuse-refresh-tokens");
/**
* Set the time-to-live for a refresh token.
*/
public static final String REFRESH_TOKEN_TIME_TO_LIVE = TOKEN_SETTINGS_NAMESPACE.concat("refresh-token-time-to-live");
/**
* Set the {@link SignatureAlgorithm JWS} algorithm for signing the {@link OidcIdToken ID Token}.
*/
public static final String ID_TOKEN_SIGNATURE_ALGORITHM = TOKEN_SETTINGS_NAMESPACE.concat("id-token-signature-algorithm");
private Token() {
}
}
}

View File

@@ -15,39 +15,22 @@
*/
package org.springframework.security.oauth2.server.authorization.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.Assert;
/**
* A facility for provider configuration settings.
*
* @author Daniel Garnier-Moiroux
* @author Joe Grandja
* @since 0.1.0
* @see Settings
* @see AbstractSettings
* @see ConfigurationSettingNames.Provider
*/
public class ProviderSettings extends Settings {
private static final String PROVIDER_SETTING_BASE = "setting.provider.";
public static final String ISSUER = PROVIDER_SETTING_BASE.concat("issuer");
public static final String AUTHORIZATION_ENDPOINT = PROVIDER_SETTING_BASE.concat("authorization-endpoint");
public static final String TOKEN_ENDPOINT = PROVIDER_SETTING_BASE.concat("token-endpoint");
public static final String JWK_SET_ENDPOINT = PROVIDER_SETTING_BASE.concat("jwk-set-endpoint");
public static final String TOKEN_REVOCATION_ENDPOINT = PROVIDER_SETTING_BASE.concat("token-revocation-endpoint");
public static final String TOKEN_INTROSPECTION_ENDPOINT = PROVIDER_SETTING_BASE.concat("token-introspection-endpoint");
public static final String OIDC_CLIENT_REGISTRATION_ENDPOINT = PROVIDER_SETTING_BASE.concat("oidc-client-registration-endpoint");
public final class ProviderSettings extends AbstractSettings {
/**
* Constructs a {@code ProviderSettings}.
*/
public ProviderSettings() {
this(defaultSettings());
}
/**
* Constructs a {@code ProviderSettings} using the provided parameters.
*
* @param settings the initial settings
*/
public ProviderSettings(Map<String, Object> settings) {
private ProviderSettings(Map<String, Object> settings) {
super(settings);
}
@@ -56,18 +39,8 @@ public class ProviderSettings extends Settings {
*
* @return the URL of the Provider's Issuer Identifier
*/
public String issuer() {
return setting(ISSUER);
}
/**
* Sets the URL the Provider uses as its Issuer Identifier.
*
* @param issuer the URL the Provider uses as its Issuer Identifier.
* @return the {@link ProviderSettings} for further configuration
*/
public ProviderSettings issuer(String issuer) {
return setting(ISSUER, issuer);
public String getIssuer() {
return getSetting(ConfigurationSettingNames.Provider.ISSUER);
}
/**
@@ -75,18 +48,8 @@ public class ProviderSettings extends Settings {
*
* @return the Authorization endpoint
*/
public String authorizationEndpoint() {
return setting(AUTHORIZATION_ENDPOINT);
}
/**
* Sets the Provider's OAuth 2.0 Authorization endpoint.
*
* @param authorizationEndpoint the Authorization endpoint
* @return the {@link ProviderSettings} for further configuration
*/
public ProviderSettings authorizationEndpoint(String authorizationEndpoint) {
return setting(AUTHORIZATION_ENDPOINT, authorizationEndpoint);
public String getAuthorizationEndpoint() {
return getSetting(ConfigurationSettingNames.Provider.AUTHORIZATION_ENDPOINT);
}
/**
@@ -94,18 +57,8 @@ public class ProviderSettings extends Settings {
*
* @return the Token endpoint
*/
public String tokenEndpoint() {
return setting(TOKEN_ENDPOINT);
}
/**
* Sets the Provider's OAuth 2.0 Token endpoint.
*
* @param tokenEndpoint the Token endpoint
* @return the {@link ProviderSettings} for further configuration
*/
public ProviderSettings tokenEndpoint(String tokenEndpoint) {
return setting(TOKEN_ENDPOINT, tokenEndpoint);
public String getTokenEndpoint() {
return getSetting(ConfigurationSettingNames.Provider.TOKEN_ENDPOINT);
}
/**
@@ -113,18 +66,8 @@ public class ProviderSettings extends Settings {
*
* @return the JWK Set endpoint
*/
public String jwkSetEndpoint() {
return setting(JWK_SET_ENDPOINT);
}
/**
* Sets the Provider's JWK Set endpoint.
*
* @param jwkSetEndpoint the JWK Set endpoint
* @return the {@link ProviderSettings} for further configuration
*/
public ProviderSettings jwkSetEndpoint(String jwkSetEndpoint) {
return setting(JWK_SET_ENDPOINT, jwkSetEndpoint);
public String getJwkSetEndpoint() {
return getSetting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT);
}
/**
@@ -132,18 +75,8 @@ public class ProviderSettings extends Settings {
*
* @return the Token Revocation endpoint
*/
public String tokenRevocationEndpoint() {
return setting(TOKEN_REVOCATION_ENDPOINT);
}
/**
* Sets the Provider's OAuth 2.0 Token Revocation endpoint.
*
* @param tokenRevocationEndpoint the Token Revocation endpoint
* @return the {@link ProviderSettings} for further configuration
*/
public ProviderSettings tokenRevocationEndpoint(String tokenRevocationEndpoint) {
return setting(TOKEN_REVOCATION_ENDPOINT, tokenRevocationEndpoint);
public String getTokenRevocationEndpoint() {
return getSetting(ConfigurationSettingNames.Provider.TOKEN_REVOCATION_ENDPOINT);
}
/**
@@ -151,18 +84,8 @@ public class ProviderSettings extends Settings {
*
* @return the Token Introspection endpoint
*/
public String tokenIntrospectionEndpoint() {
return setting(TOKEN_INTROSPECTION_ENDPOINT);
}
/**
* Sets the Provider's OAuth 2.0 Token Introspection endpoint.
*
* @param tokenIntrospectionEndpoint the Token Introspection endpoint
* @return the {@link ProviderSettings} for further configuration
*/
public ProviderSettings tokenIntrospectionEndpoint(String tokenIntrospectionEndpoint) {
return setting(TOKEN_INTROSPECTION_ENDPOINT, tokenIntrospectionEndpoint);
public String getTokenIntrospectionEndpoint() {
return getSetting(ConfigurationSettingNames.Provider.TOKEN_INTROSPECTION_ENDPOINT);
}
/**
@@ -170,28 +93,145 @@ public class ProviderSettings extends Settings {
*
* @return the OpenID Connect 1.0 Client Registration endpoint
*/
public String oidcClientRegistrationEndpoint() {
return setting(OIDC_CLIENT_REGISTRATION_ENDPOINT);
public String getOidcClientRegistrationEndpoint() {
return getSetting(ConfigurationSettingNames.Provider.OIDC_CLIENT_REGISTRATION_ENDPOINT);
}
/**
* Sets the Provider's OpenID Connect 1.0 Client Registration endpoint.
* Returns the Provider's OpenID Connect 1.0 UserInfo endpoint. The default is {@code /userinfo}.
*
* @param oidcClientRegistrationEndpoint the OpenID Connect 1.0 Client Registration endpoint
* @return the {@link ProviderSettings} for further configuration
* @return the OpenID Connect 1.0 UserInfo endpoint
*/
public ProviderSettings oidcClientRegistrationEndpoint(String oidcClientRegistrationEndpoint) {
return setting(OIDC_CLIENT_REGISTRATION_ENDPOINT, oidcClientRegistrationEndpoint);
public String getOidcUserInfoEndpoint() {
return getSetting(ConfigurationSettingNames.Provider.OIDC_USER_INFO_ENDPOINT);
}
protected static Map<String, Object> defaultSettings() {
Map<String, Object> settings = new HashMap<>();
settings.put(AUTHORIZATION_ENDPOINT, "/oauth2/authorize");
settings.put(TOKEN_ENDPOINT, "/oauth2/token");
settings.put(JWK_SET_ENDPOINT, "/oauth2/jwks");
settings.put(TOKEN_REVOCATION_ENDPOINT, "/oauth2/revoke");
settings.put(TOKEN_INTROSPECTION_ENDPOINT, "/oauth2/introspect");
settings.put(OIDC_CLIENT_REGISTRATION_ENDPOINT, "/connect/register");
return settings;
/**
* Constructs a new {@link Builder} with the default settings.
*
* @return the {@link Builder}
*/
public static Builder builder() {
return new Builder()
.authorizationEndpoint("/oauth2/authorize")
.tokenEndpoint("/oauth2/token")
.jwkSetEndpoint("/oauth2/jwks")
.tokenRevocationEndpoint("/oauth2/revoke")
.tokenIntrospectionEndpoint("/oauth2/introspect")
.oidcClientRegistrationEndpoint("/connect/register")
.oidcUserInfoEndpoint("/userinfo");
}
/**
* Constructs a new {@link Builder} with the provided settings.
*
* @param settings the settings to initialize the builder
* @return the {@link Builder}
*/
public static Builder withSettings(Map<String, Object> settings) {
Assert.notEmpty(settings, "settings cannot be empty");
return new Builder()
.settings(s -> s.putAll(settings));
}
/**
* A builder for {@link ProviderSettings}.
*/
public static class Builder extends AbstractBuilder<ProviderSettings, Builder> {
private Builder() {
}
/**
* Sets the URL the Provider uses as its Issuer Identifier.
*
* @param issuer the URL the Provider uses as its Issuer Identifier.
* @return the {@link Builder} for further configuration
*/
public Builder issuer(String issuer) {
return setting(ConfigurationSettingNames.Provider.ISSUER, issuer);
}
/**
* Sets the Provider's 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);
}
/**
* Sets the Provider's 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);
}
/**
* Sets the Provider's 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);
}
/**
* Sets the Provider's 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);
}
/**
* Sets the Provider's 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);
}
/**
* Sets the Provider's 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);
}
/**
* Sets the Provider's 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);
}
/**
* Builds the {@link ProviderSettings}.
*
* @return the {@link ProviderSettings}
*/
@Override
public ProviderSettings build() {
return new ProviderSettings(getSettings());
}
}
}

View File

@@ -1,104 +0,0 @@
/*
* Copyright 2020 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;
import org.springframework.security.oauth2.core.Version;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* A facility for configuration settings.
*
* @author Joe Grandja
* @since 0.0.2
*/
public class Settings implements Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private final Map<String, Object> settings;
/**
* Constructs a {@code Settings}.
*/
public Settings() {
this.settings = new HashMap<>();
}
/**
* Constructs a {@code Settings} using the provided parameters.
*
* @param settings the initial settings
*/
public Settings(Map<String, Object> settings) {
Assert.notNull(settings, "settings cannot be null");
this.settings = new HashMap<>(settings);
}
/**
* Returns a configuration setting.
*
* @param name the name of the setting
* @param <T> the type of the setting
* @return the value of the setting, or {@code null} if not available
*/
@SuppressWarnings("unchecked")
public <T> T setting(String name) {
Assert.hasText(name, "name cannot be empty");
return (T) this.settings.get(name);
}
/**
* Sets a configuration setting.
*
* @param name the name of the setting
* @param value the value of the setting
* @param <T> the type of the {@link Settings}
* @return the {@link Settings}
*/
@SuppressWarnings("unchecked")
public <T extends Settings> T setting(String name, Object value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
this.settings.put(name, value);
return (T) this;
}
/**
* Returns a {@code Map} of the configuration settings.
*
* @return a {@code Map} of the configuration settings
*/
public Map<String, Object> settings() {
return this.settings;
}
/**
* A {@code Consumer} of the configuration settings {@code Map}
* allowing the ability to add, replace, or remove.
*
* @param settingsConsumer a {@link Consumer} of the configuration settings {@code Map}
* @param <T> the type of the {@link Settings}
* @return the {@link Settings}
*/
@SuppressWarnings("unchecked")
public <T extends Settings> T settings(Consumer<Map<String, Object>> settingsConsumer) {
settingsConsumer.accept(this.settings);
return (T) this;
}
}

View File

@@ -16,7 +16,6 @@
package org.springframework.security.oauth2.server.authorization.config;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
@@ -28,28 +27,12 @@ import org.springframework.util.Assert;
*
* @author Joe Grandja
* @since 0.0.2
* @see Settings
* @see AbstractSettings
* @see ConfigurationSettingNames.Token
*/
public class TokenSettings extends Settings {
private static final String TOKEN_SETTING_BASE = "setting.token.";
public static final String ACCESS_TOKEN_TIME_TO_LIVE = TOKEN_SETTING_BASE.concat("access-token-time-to-live");
public static final String REUSE_REFRESH_TOKENS = TOKEN_SETTING_BASE.concat("reuse-refresh-tokens");
public static final String REFRESH_TOKEN_TIME_TO_LIVE = TOKEN_SETTING_BASE.concat("refresh-token-time-to-live");
public static final String ID_TOKEN_SIGNATURE_ALGORITHM = TOKEN_SETTING_BASE.concat("id-token-signature-algorithm");
public final class TokenSettings extends AbstractSettings {
/**
* Constructs a {@code TokenSettings}.
*/
public TokenSettings() {
this(defaultSettings());
}
/**
* Constructs a {@code TokenSettings} using the provided parameters.
*
* @param settings the initial settings
*/
public TokenSettings(Map<String, Object> settings) {
private TokenSettings(Map<String, Object> settings) {
super(settings);
}
@@ -58,41 +41,16 @@ public class TokenSettings extends Settings {
*
* @return the time-to-live for an access token
*/
public Duration accessTokenTimeToLive() {
return setting(ACCESS_TOKEN_TIME_TO_LIVE);
}
/**
* Set the time-to-live for an access token. Must be greater than {@code Duration.ZERO}.
*
* @param accessTokenTimeToLive the time-to-live for an access token
* @return the {@link TokenSettings}
*/
public TokenSettings accessTokenTimeToLive(Duration accessTokenTimeToLive) {
Assert.notNull(accessTokenTimeToLive, "accessTokenTimeToLive cannot be null");
Assert.isTrue(accessTokenTimeToLive.getSeconds() > 0, "accessTokenTimeToLive must be greater than Duration.ZERO");
setting(ACCESS_TOKEN_TIME_TO_LIVE, accessTokenTimeToLive);
return this;
public Duration getAccessTokenTimeToLive() {
return getSetting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE);
}
/**
* Returns {@code true} if refresh tokens are reused when returning the access token response,
* or {@code false} if a new refresh token is issued. The default is {@code true}.
*/
public boolean reuseRefreshTokens() {
return setting(REUSE_REFRESH_TOKENS);
}
/**
* Set to {@code true} if refresh tokens are reused when returning the access token response,
* or {@code false} if a new refresh token is issued.
*
* @param reuseRefreshTokens {@code true} to reuse refresh tokens, {@code false} to issue new refresh tokens
* @return the {@link TokenSettings}
*/
public TokenSettings reuseRefreshTokens(boolean reuseRefreshTokens) {
setting(REUSE_REFRESH_TOKENS, reuseRefreshTokens);
return this;
public boolean isReuseRefreshTokens() {
return getSetting(ConfigurationSettingNames.Token.REUSE_REFRESH_TOKENS);
}
/**
@@ -100,21 +58,8 @@ public class TokenSettings extends Settings {
*
* @return the time-to-live for a refresh token
*/
public Duration refreshTokenTimeToLive() {
return setting(REFRESH_TOKEN_TIME_TO_LIVE);
}
/**
* Set the time-to-live for a refresh token. Must be greater than {@code Duration.ZERO}.
*
* @param refreshTokenTimeToLive the time-to-live for a refresh token
* @return the {@link TokenSettings}
*/
public TokenSettings refreshTokenTimeToLive(Duration refreshTokenTimeToLive) {
Assert.notNull(refreshTokenTimeToLive, "refreshTokenTimeToLive cannot be null");
Assert.isTrue(refreshTokenTimeToLive.getSeconds() > 0, "refreshTokenTimeToLive must be greater than Duration.ZERO");
setting(REFRESH_TOKEN_TIME_TO_LIVE, refreshTokenTimeToLive);
return this;
public Duration getRefreshTokenTimeToLive() {
return getSetting(ConfigurationSettingNames.Token.REFRESH_TOKEN_TIME_TO_LIVE);
}
/**
@@ -123,29 +68,99 @@ public class TokenSettings extends Settings {
*
* @return the {@link SignatureAlgorithm JWS} algorithm for signing the {@link OidcIdToken ID Token}
*/
public SignatureAlgorithm idTokenSignatureAlgorithm() {
return setting(ID_TOKEN_SIGNATURE_ALGORITHM);
public SignatureAlgorithm getIdTokenSignatureAlgorithm() {
return getSetting(ConfigurationSettingNames.Token.ID_TOKEN_SIGNATURE_ALGORITHM);
}
/**
* Sets the {@link SignatureAlgorithm JWS} algorithm for signing the {@link OidcIdToken ID Token}.
* Constructs a new {@link Builder} with the default settings.
*
* @param idTokenSignatureAlgorithm the {@link SignatureAlgorithm JWS} algorithm for signing the {@link OidcIdToken ID Token}
* @return the {@link TokenSettings}
* @return the {@link Builder}
*/
public TokenSettings idTokenSignatureAlgorithm(SignatureAlgorithm idTokenSignatureAlgorithm) {
Assert.notNull(idTokenSignatureAlgorithm, "idTokenSignatureAlgorithm cannot be null");
setting(ID_TOKEN_SIGNATURE_ALGORITHM, idTokenSignatureAlgorithm);
return this;
public static Builder builder() {
return new Builder()
.accessTokenTimeToLive(Duration.ofMinutes(5))
.reuseRefreshTokens(true)
.refreshTokenTimeToLive(Duration.ofMinutes(60))
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256);
}
protected static Map<String, Object> defaultSettings() {
Map<String, Object> settings = new HashMap<>();
settings.put(ACCESS_TOKEN_TIME_TO_LIVE, Duration.ofMinutes(5));
settings.put(REUSE_REFRESH_TOKENS, true);
settings.put(REFRESH_TOKEN_TIME_TO_LIVE, Duration.ofMinutes(60));
settings.put(ID_TOKEN_SIGNATURE_ALGORITHM, SignatureAlgorithm.RS256);
return settings;
/**
* Constructs a new {@link Builder} with the provided settings.
*
* @param settings the settings to initialize the builder
* @return the {@link Builder}
*/
public static Builder withSettings(Map<String, Object> settings) {
Assert.notEmpty(settings, "settings cannot be empty");
return new Builder()
.settings(s -> s.putAll(settings));
}
/**
* A builder for {@link TokenSettings}.
*/
public static class Builder extends AbstractBuilder<TokenSettings, Builder> {
private Builder() {
}
/**
* Set the time-to-live for an access token. Must be greater than {@code Duration.ZERO}.
*
* @param accessTokenTimeToLive the time-to-live for an access token
* @return the {@link Builder} for further configuration
*/
public Builder accessTokenTimeToLive(Duration accessTokenTimeToLive) {
Assert.notNull(accessTokenTimeToLive, "accessTokenTimeToLive cannot be null");
Assert.isTrue(accessTokenTimeToLive.getSeconds() > 0, "accessTokenTimeToLive must be greater than Duration.ZERO");
return setting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE, accessTokenTimeToLive);
}
/**
* Set to {@code true} if refresh tokens are reused when returning the access token response,
* or {@code false} if a new refresh token is issued.
*
* @param reuseRefreshTokens {@code true} to reuse refresh tokens, {@code false} to issue new refresh tokens
* @return the {@link Builder} for further configuration
*/
public Builder reuseRefreshTokens(boolean reuseRefreshTokens) {
return setting(ConfigurationSettingNames.Token.REUSE_REFRESH_TOKENS, reuseRefreshTokens);
}
/**
* Set the time-to-live for a refresh token. Must be greater than {@code Duration.ZERO}.
*
* @param refreshTokenTimeToLive the time-to-live for a refresh token
* @return the {@link Builder} for further configuration
*/
public Builder refreshTokenTimeToLive(Duration refreshTokenTimeToLive) {
Assert.notNull(refreshTokenTimeToLive, "refreshTokenTimeToLive cannot be null");
Assert.isTrue(refreshTokenTimeToLive.getSeconds() > 0, "refreshTokenTimeToLive must be greater than Duration.ZERO");
return setting(ConfigurationSettingNames.Token.REFRESH_TOKEN_TIME_TO_LIVE, refreshTokenTimeToLive);
}
/**
* Sets the {@link SignatureAlgorithm JWS} algorithm for signing the {@link OidcIdToken ID Token}.
*
* @param idTokenSignatureAlgorithm the {@link SignatureAlgorithm JWS} algorithm for signing the {@link OidcIdToken ID Token}
* @return the {@link Builder} for further configuration
*/
public Builder idTokenSignatureAlgorithm(SignatureAlgorithm idTokenSignatureAlgorithm) {
Assert.notNull(idTokenSignatureAlgorithm, "idTokenSignatureAlgorithm cannot be null");
return setting(ConfigurationSettingNames.Token.ID_TOKEN_SIGNATURE_ALGORITHM, idTokenSignatureAlgorithm);
}
/**
* Builds the {@link TokenSettings}.
*
* @return the {@link TokenSettings}
*/
@Override
public TokenSettings build() {
return new TokenSettings(getSettings());
}
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.jackson2;
import java.time.Duration;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.module.SimpleModule;
@@ -71,6 +72,7 @@ public class OAuth2AuthorizationServerJackson2Module extends SimpleModule {
context.setMixInAnnotations(Collections.unmodifiableMap(Collections.emptyMap()).getClass(),
UnmodifiableMapMixin.class);
context.setMixInAnnotations(HashSet.class, HashSetMixin.class);
context.setMixInAnnotations(LinkedHashSet.class, HashSetMixin.class);
context.setMixInAnnotations(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class);
context.setMixInAnnotations(Duration.class, DurationMixin.class);
context.setMixInAnnotations(SignatureAlgorithm.class, SignatureAlgorithmMixin.class);

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2020-2021 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.time.Instant;
import java.util.Collections;
import java.util.Set;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JoseHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* TODO
* This class is mostly a straight copy from {@code org.springframework.security.oauth2.server.authorization.authentication.JwtUtils}.
* It should be consolidated when we introduce a token generator abstraction.
*
* Utility methods used by the {@link AuthenticationProvider}'s when issuing {@link Jwt}'s.
*
* @author Ovidiu Popa
* @since 0.2.1
*/
final class JwtUtils {
private JwtUtils() {
}
static JoseHeader.Builder headers() {
return JoseHeader.withAlgorithm(SignatureAlgorithm.RS256);
}
static JwtClaimsSet.Builder accessTokenClaims(RegisteredClient registeredClient,
String issuer, String subject, Set<String> authorizedScopes) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
// @formatter:off
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder();
if (StringUtils.hasText(issuer)) {
claimsBuilder.issuer(issuer);
}
claimsBuilder
.subject(subject)
.audience(Collections.singletonList(registeredClient.getClientId()))
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.notBefore(issuedAt);
if (!CollectionUtils.isEmpty(authorizedScopes)) {
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, authorizedScopes);
}
// @formatter:on
return claimsBuilder;
}
}

View File

@@ -19,7 +19,7 @@ import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
/**
* Utility methods for the OpenID Connect 1.0 {@link AuthenticationProvider}'s.

View File

@@ -15,11 +15,18 @@
*/
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
@@ -29,46 +36,60 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JoseHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
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.config.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriComponentsBuilder;
/**
* An {@link AuthenticationProvider} implementation for OpenID Connect Dynamic Client Registration 1.0.
* An {@link AuthenticationProvider} implementation for OpenID Connect 1.0 Dynamic Client Registration (and Configuration) Endpoint.
*
* @author Ovidiu Popa
* @author Joe Grandja
* @since 0.1.1
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @see JwtEncoder
* @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 class OidcClientRegistrationAuthenticationProvider implements AuthenticationProvider {
public final class OidcClientRegistrationAuthenticationProvider implements AuthenticationProvider {
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_AUTHORIZED_SCOPE = "client.create";
private static final String DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE = "client.create";
private static final String DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE = "client.read";
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
private JwtEncoder jwtEncoder;
private ProviderSettings providerSettings;
/**
* Constructs an {@code OidcClientRegistrationAuthenticationProvider} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
* @deprecated Use {@link #OidcClientRegistrationAuthenticationProvider(RegisteredClientRepository, OAuth2AuthorizationService, JwtEncoder)} instead
*/
@Deprecated
public OidcClientRegistrationAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
@@ -77,18 +98,46 @@ public class OidcClientRegistrationAuthenticationProvider implements Authenticat
this.authorizationService = authorizationService;
}
/**
* Constructs an {@code OidcClientRegistrationAuthenticationProvider} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
* @param jwtEncoder the jwt encoder
*/
public OidcClientRegistrationAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
this.registeredClientRepository = registeredClientRepository;
this.authorizationService = authorizationService;
this.jwtEncoder = jwtEncoder;
}
@Deprecated
@Autowired(required = false)
protected void setJwtEncoder(JwtEncoder jwtEncoder) {
this.jwtEncoder = jwtEncoder;
}
@Autowired
protected void setProviderSettings(ProviderSettings providerSettings) {
this.providerSettings = providerSettings;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication =
(OidcClientRegistrationAuthenticationToken) authentication;
// Validate the "initial" access token
// Validate the "initial" or "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(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
}
String accessTokenValue = accessTokenAuthentication.getToken().getTokenValue();
@@ -96,21 +145,62 @@ public class OidcClientRegistrationAuthenticationProvider implements Authenticat
OAuth2Authorization authorization = this.authorizationService.findByToken(
accessTokenValue, OAuth2TokenType.ACCESS_TOKEN);
if (authorization == null) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
}
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
if (!authorizedAccessToken.isActive()) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
}
if (!isAuthorized(authorizedAccessToken)) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INSUFFICIENT_SCOPE));
return clientRegistrationAuthentication.getClientRegistration() != null ?
registerClient(clientRegistrationAuthentication, authorization) :
findRegistration(clientRegistrationAuthentication, authorization);
}
@Override
public boolean supports(Class<?> authentication) {
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);
}
RegisteredClient registeredClient = create(clientRegistrationAuthentication.getClientRegistration());
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
OidcClientRegistration clientRegistration = buildRegistration(registeredClient).build();
return new OidcClientRegistrationAuthenticationToken(
(Authentication) clientRegistrationAuthentication.getPrincipal(), clientRegistration);
}
private OidcClientRegistrationAuthenticationToken registerClient(OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication,
OAuth2Authorization authorization) {
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
checkScopeForRegistration(authorizedAccessToken);
if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getRedirectUris())) {
// TODO Add OAuth2ErrorCodes.INVALID_REDIRECT_URI
throw new OAuth2AuthenticationException("invalid_redirect_uri");
}
RegisteredClient registeredClient = createClient(clientRegistrationAuthentication.getClientRegistration());
this.registeredClientRepository.save(registeredClient);
OAuth2Authorization registeredClientAuthorization = registerAccessToken(registeredClient);
// Invalidate the "initial" access token as it can only be used once
authorization = OidcAuthenticationProviderUtils.invalidate(authorization, authorizedAccessToken.getToken());
if (authorization.getRefreshToken() != null) {
@@ -118,72 +208,48 @@ public class OidcClientRegistrationAuthenticationProvider implements Authenticat
}
this.authorizationService.save(authorization);
OidcClientRegistration clientRegistration = buildRegistration(registeredClient)
.registrationAccessToken(registeredClientAuthorization.getAccessToken().getToken().getTokenValue())
.build();
return new OidcClientRegistrationAuthenticationToken(
accessTokenAuthentication, convert(registeredClient));
(Authentication) clientRegistrationAuthentication.getPrincipal(), clientRegistration);
}
@Override
public boolean supports(Class<?> authentication) {
return OidcClientRegistrationAuthenticationToken.class.isAssignableFrom(authentication);
}
private OAuth2Authorization registerAccessToken(RegisteredClient registeredClient) {
JoseHeader headers = JwtUtils.headers().build();
@SuppressWarnings("unchecked")
private static boolean isAuthorized(OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken) {
Object scope = authorizedAccessToken.getClaims().get(OAuth2ParameterNames.SCOPE);
return scope != null && ((Collection<String>) scope).contains(DEFAULT_AUTHORIZED_SCOPE);
}
Set<String> authorizedScopes = new HashSet<>();
authorizedScopes.add(DEFAULT_CLIENT_CONFIGURATION_AUTHORIZED_SCOPE);
authorizedScopes = Collections.unmodifiableSet(authorizedScopes);
JwtClaimsSet claims = JwtUtils.accessTokenClaims(
registeredClient, this.providerSettings.getIssuer(), registeredClient.getClientId(), authorizedScopes)
.build();
Jwt registrationAccessToken = this.jwtEncoder.encode(headers, claims);
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
registrationAccessToken.getTokenValue(), registrationAccessToken.getIssuedAt(),
registrationAccessToken.getExpiresAt(), authorizedScopes);
private static RegisteredClient create(OidcClientRegistration clientRegistration) {
// @formatter:off
RegisteredClient.Builder builder = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId(CLIENT_ID_GENERATOR.generateKey())
.clientIdIssuedAt(Instant.now())
.clientSecret(CLIENT_SECRET_GENERATOR.generateKey())
.clientName(clientRegistration.getClientName());
if ("client_secret_post".equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
// TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
builder.clientAuthenticationMethod(ClientAuthenticationMethod.POST);
} else {
// TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
builder.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC);
}
// TODO Validate redirect_uris and throw OAuth2ErrorCodes2.INVALID_REDIRECT_URI on error
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()));
}
builder
.clientSettings(clientSettings ->
clientSettings
.requireProofKey(true)
.requireUserConsent(true))
.tokenSettings(tokenSettings ->
tokenSettings
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256));
return builder.build();
OAuth2Authorization registeredClientAuthorization = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(registeredClient.getClientId())
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.token(accessToken,
(metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, registrationAccessToken.getClaims()))
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes)
.build();
// @formatter:on
this.authorizationService.save(registeredClientAuthorization);
return registeredClientAuthorization;
}
private static OidcClientRegistration convert(RegisteredClient registeredClient) {
private OidcClientRegistration.Builder buildRegistration(RegisteredClient registeredClient) {
// @formatter:off
OidcClientRegistration.Builder builder = OidcClientRegistration.builder()
.clientId(registeredClient.getClientId())
@@ -207,9 +273,103 @@ public class OidcClientRegistrationAuthenticationProvider implements Authenticat
scopes.addAll(registeredClient.getScopes()));
}
String registrationClientUri = UriComponentsBuilder.fromUriString(this.providerSettings.getIssuer())
.path(this.providerSettings.getOidcClientRegistrationEndpoint())
.queryParam(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
.toUriString();
builder
.tokenEndpointAuthenticationMethod(registeredClient.getClientAuthenticationMethods().iterator().next().getValue())
.idTokenSignedResponseAlgorithm(registeredClient.getTokenSettings().idTokenSignatureAlgorithm().getName());
.idTokenSignedResponseAlgorithm(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName())
.registrationClientUrl(registrationClientUri);
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();
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);
}
}
private static boolean isValidRedirectUris(List<String> redirectUris) {
if (CollectionUtils.isEmpty(redirectUris)) {
return true;
}
for (String redirectUri : redirectUris) {
try {
URI validRedirectUri = new URI(redirectUri);
if (validRedirectUri.getFragment() != null) {
return false;
}
} catch (URISyntaxException ex) {
return false;
}
}
return true;
}
private static RegisteredClient createClient(OidcClientRegistration clientRegistration) {
// @formatter:off
RegisteredClient.Builder builder = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId(CLIENT_ID_GENERATOR.generateKey())
.clientIdIssuedAt(Instant.now())
.clientSecret(CLIENT_SECRET_GENERATOR.generateKey())
.clientName(clientRegistration.getClientName());
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
builder.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST);
} else {
builder.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
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()));
}
builder
.clientSettings(ClientSettings.builder()
.requireProofKey(true)
.requireAuthorizationConsent(true)
.build())
.tokenSettings(TokenSettings.builder()
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
.build());
return builder.build();
// @formatter:on

View File

@@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.authorization.oidc.authentica
import java.util.Collections;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.Version;
@@ -24,9 +25,10 @@ import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
import org.springframework.util.Assert;
/**
* An {@link Authentication} implementation used for OpenID Connect Dynamic Client Registration 1.0.
* An {@link Authentication} implementation used for OpenID Connect 1.0 Dynamic Client Registration (and Configuration) Endpoint.
*
* @author Joe Grandja
* @author Ovidiu Popa
* @since 0.1.1
* @see AbstractAuthenticationToken
* @see OidcClientRegistration
@@ -36,6 +38,7 @@ public class OidcClientRegistrationAuthenticationToken extends AbstractAuthentic
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private final Authentication principal;
private final OidcClientRegistration clientRegistration;
private final String clientId;
/**
* Constructs an {@code OidcClientRegistrationAuthenticationToken} using the provided parameters.
@@ -49,6 +52,24 @@ public class OidcClientRegistrationAuthenticationToken extends AbstractAuthentic
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
this.principal = principal;
this.clientRegistration = clientRegistration;
this.clientId = null;
setAuthenticated(principal.isAuthenticated());
}
/**
* Constructs an {@code OidcClientRegistrationAuthenticationToken} using the provided parameters.
*
* @param principal the authenticated principal
* @param clientId the client identifier
* @since 0.2.1
*/
public OidcClientRegistrationAuthenticationToken(Authentication principal, String clientId) {
super(Collections.emptyList());
Assert.notNull(principal, "principal cannot be null");
Assert.hasText(clientId, "clientId cannot be empty");
this.principal = principal;
this.clientRegistration = null;
this.clientId = clientId;
setAuthenticated(principal.isAuthenticated());
}
@@ -71,4 +92,15 @@ public class OidcClientRegistrationAuthenticationToken extends AbstractAuthentic
return this.clientRegistration;
}
/**
* Returns the client identifier.
*
* @return the client identifier
* @since 0.2.1
*/
@Nullable
public String getClientId() {
return this.clientId;
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2020-2021 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.Map;
import java.util.function.Function;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.authentication.OAuth2AuthenticationContext;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.util.Assert;
/**
* An {@link OAuth2AuthenticationContext} that holds an {@link OidcUserInfoAuthenticationToken} and additional information
* and is used when mapping claims to an instance of {@link OidcUserInfo}.
*
* @author Joe Grandja
* @since 0.2.1
* @see OAuth2AuthenticationContext
* @see OidcUserInfoAuthenticationProvider#setUserInfoMapper(Function)
*/
public final class OidcUserInfoAuthenticationContext extends OAuth2AuthenticationContext {
private OidcUserInfoAuthenticationContext(Map<Object, Object> context) {
super(context);
}
/**
* Returns the {@link OAuth2AccessToken OAuth 2.0 Access Token}.
*
* @return the {@link OAuth2AccessToken}
*/
public OAuth2AccessToken getAccessToken() {
return get(OAuth2AccessToken.class);
}
/**
* Returns the {@link OAuth2Authorization authorization}.
*
* @return the {@link OAuth2Authorization}
*/
public OAuth2Authorization getAuthorization() {
return get(OAuth2Authorization.class);
}
/**
* Constructs a new {@link Builder} with the provided {@link OidcUserInfoAuthenticationToken}.
*
* @param authentication the {@link OidcUserInfoAuthenticationToken}
* @return the {@link Builder}
*/
public static Builder with(OidcUserInfoAuthenticationToken authentication) {
return new Builder(authentication);
}
/**
* A builder for {@link OidcUserInfoAuthenticationContext}.
*/
public static final class Builder extends AbstractBuilder<OidcUserInfoAuthenticationContext, Builder> {
private Builder(OidcUserInfoAuthenticationToken authentication) {
super(authentication);
}
/**
* Sets the {@link OAuth2AccessToken OAuth 2.0 Access Token}.
*
* @param accessToken the {@link OAuth2AccessToken}
* @return the {@link Builder} for further configuration
*/
public Builder accessToken(OAuth2AccessToken accessToken) {
return put(OAuth2AccessToken.class, accessToken);
}
/**
* Sets the {@link OAuth2Authorization authorization}.
*
* @param authorization the {@link OAuth2Authorization}
* @return the {@link Builder} for further configuration
*/
public Builder authorization(OAuth2Authorization authorization) {
return put(OAuth2Authorization.class, authorization);
}
/**
* Builds a new {@link OidcUserInfoAuthenticationContext}.
*
* @return the {@link OidcUserInfoAuthenticationContext}
*/
public OidcUserInfoAuthenticationContext build() {
Assert.notNull(get(OAuth2AccessToken.class), "accessToken cannot be null");
Assert.notNull(get(OAuth2Authorization.class), "authorization cannot be null");
return new OidcUserInfoAuthenticationContext(getContext());
}
}
}

View File

@@ -0,0 +1,197 @@
/*
* Copyright 2020-2021 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.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
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.OAuth2TokenType;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} implementation for OpenID Connect 1.0 UserInfo Endpoint.
*
* @author Steve Riesenberg
* @since 0.2.1
* @see OAuth2AuthorizationService
* @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 OAuth2AuthorizationService authorizationService;
private Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper = new DefaultOidcUserInfoMapper();
/**
* Constructs an {@code OidcUserInfoAuthenticationProvider} using the provided parameters.
*
* @param authorizationService the authorization service
*/
public OidcUserInfoAuthenticationProvider(OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.authorizationService = authorizationService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OidcUserInfoAuthenticationToken userInfoAuthentication =
(OidcUserInfoAuthenticationToken) authentication;
AbstractOAuth2TokenAuthenticationToken<?> accessTokenAuthentication = null;
if (AbstractOAuth2TokenAuthenticationToken.class.isAssignableFrom(userInfoAuthentication.getPrincipal().getClass())) {
accessTokenAuthentication = (AbstractOAuth2TokenAuthenticationToken<?>) userInfoAuthentication.getPrincipal();
}
if (accessTokenAuthentication == null || !accessTokenAuthentication.isAuthenticated()) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
}
String accessTokenValue = accessTokenAuthentication.getToken().getTokenValue();
OAuth2Authorization authorization = this.authorizationService.findByToken(
accessTokenValue, OAuth2TokenType.ACCESS_TOKEN);
if (authorization == null) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
}
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
if (!authorizedAccessToken.isActive()) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
}
if (!authorizedAccessToken.getToken().getScopes().contains(OidcScopes.OPENID)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
}
OAuth2Authorization.Token<OidcIdToken> idToken = authorization.getToken(OidcIdToken.class);
if (idToken == null) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
}
OidcUserInfoAuthenticationContext authenticationContext =
OidcUserInfoAuthenticationContext.with(userInfoAuthentication)
.accessToken(authorizedAccessToken.getToken())
.authorization(authorization)
.build();
OidcUserInfo userInfo = this.userInfoMapper.apply(authenticationContext);
return new OidcUserInfoAuthenticationToken(accessTokenAuthentication, userInfo);
}
@Override
public boolean supports(Class<?> authentication) {
return OidcUserInfoAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* Sets the {@link Function} used to extract claims from {@link OidcUserInfoAuthenticationContext}
* to an instance of {@link OidcUserInfo} for the UserInfo response.
*
* <p>
* The {@link OidcUserInfoAuthenticationContext} gives the mapper access to the {@link OidcUserInfoAuthenticationToken},
* as well as, the following context attributes:
* <ul>
* <li>{@link OidcUserInfoAuthenticationContext#getAccessToken()} containing the bearer token used to make the request.</li>
* <li>{@link OidcUserInfoAuthenticationContext#getAuthorization()} containing the {@link OidcIdToken} and
* {@link OAuth2AccessToken} associated with the bearer token used to make the request.</li>
* </ul>
*
* @param userInfoMapper the {@link Function} used to extract claims from {@link OidcUserInfoAuthenticationContext} to an instance of {@link OidcUserInfo}
*/
public void setUserInfoMapper(Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper) {
Assert.notNull(userInfoMapper, "userInfoMapper cannot be null");
this.userInfoMapper = userInfoMapper;
}
private static final class DefaultOidcUserInfoMapper implements Function<OidcUserInfoAuthenticationContext, OidcUserInfo> {
private static final List<String> EMAIL_CLAIMS = Arrays.asList(
StandardClaimNames.EMAIL,
StandardClaimNames.EMAIL_VERIFIED
);
private static final List<String> PHONE_CLAIMS = Arrays.asList(
StandardClaimNames.PHONE_NUMBER,
StandardClaimNames.PHONE_NUMBER_VERIFIED
);
private static final List<String> PROFILE_CLAIMS = Arrays.asList(
StandardClaimNames.NAME,
StandardClaimNames.FAMILY_NAME,
StandardClaimNames.GIVEN_NAME,
StandardClaimNames.MIDDLE_NAME,
StandardClaimNames.NICKNAME,
StandardClaimNames.PREFERRED_USERNAME,
StandardClaimNames.PROFILE,
StandardClaimNames.PICTURE,
StandardClaimNames.WEBSITE,
StandardClaimNames.GENDER,
StandardClaimNames.BIRTHDATE,
StandardClaimNames.ZONEINFO,
StandardClaimNames.LOCALE,
StandardClaimNames.UPDATED_AT
);
@Override
public OidcUserInfo apply(OidcUserInfoAuthenticationContext authenticationContext) {
OAuth2Authorization authorization = authenticationContext.getAuthorization();
OidcIdToken idToken = authorization.getToken(OidcIdToken.class).getToken();
OAuth2AccessToken accessToken = authenticationContext.getAccessToken();
Map<String, Object> scopeRequestedClaims = getClaimsRequestedByScope(idToken.getClaims(),
accessToken.getScopes());
return new OidcUserInfo(scopeRequestedClaims);
}
private static Map<String, Object> getClaimsRequestedByScope(Map<String, Object> claims, Set<String> requestedScopes) {
Set<String> scopeRequestedClaimNames = new HashSet<>(32);
scopeRequestedClaimNames.add(StandardClaimNames.SUB);
if (requestedScopes.contains(OidcScopes.ADDRESS)) {
scopeRequestedClaimNames.add(StandardClaimNames.ADDRESS);
}
if (requestedScopes.contains(OidcScopes.EMAIL)) {
scopeRequestedClaimNames.addAll(EMAIL_CLAIMS);
}
if (requestedScopes.contains(OidcScopes.PHONE)) {
scopeRequestedClaimNames.addAll(PHONE_CLAIMS);
}
if (requestedScopes.contains(OidcScopes.PROFILE)) {
scopeRequestedClaimNames.addAll(PROFILE_CLAIMS);
}
Map<String, Object> requestedClaims = new HashMap<>(claims);
requestedClaims.keySet().removeIf(claimName -> !scopeRequestedClaimNames.contains(claimName));
return requestedClaims;
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2020-2021 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.Collections;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.Version;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.util.Assert;
/**
* An {@link Authentication} implementation used for OpenID Connect 1.0 UserInfo Endpoint.
*
* @author Steve Riesenberg
* @since 0.2.1
* @see AbstractAuthenticationToken
* @see OidcUserInfo
* @see OidcUserInfoAuthenticationProvider
*/
public class OidcUserInfoAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private final Authentication principal;
private final OidcUserInfo userInfo;
/**
* Constructs an {@code OidcUserInfoAuthenticationToken} using the provided parameters.
*
* @param principal the principal
*/
public OidcUserInfoAuthenticationToken(Authentication principal) {
super(Collections.emptyList());
Assert.notNull(principal, "principal cannot be null");
this.principal = principal;
this.userInfo = null;
setAuthenticated(false);
}
/**
* Constructs an {@code OidcUserInfoAuthenticationToken} using the provided parameters.
*
* @param principal the authenticated principal
* @param userInfo the UserInfo claims
*/
public OidcUserInfoAuthenticationToken(Authentication principal, OidcUserInfo userInfo) {
super(Collections.emptyList());
Assert.notNull(principal, "principal cannot be null");
Assert.notNull(userInfo, "userInfo cannot be null");
this.principal = principal;
this.userInfo = userInfo;
setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return "";
}
/**
* Returns the UserInfo claims.
*
* @return the UserInfo claims
*/
public OidcUserInfo getUserInfo() {
return this.userInfo;
}
}

View File

@@ -33,29 +33,34 @@ 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.core.oidc.OidcClientRegistration;
import org.springframework.security.oauth2.core.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
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;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* A {@code Filter} that processes OpenID Connect Dynamic Client Registration 1.0 Requests.
* A {@code Filter} that processes OpenID Connect Dynamic Client Registration (and Configuration) 1.0 Requests.
*
* @author Ovidiu Popa
* @author Joe Grandja
* @since 0.1.1
* @see OidcClientRegistration
* @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 class OidcClientRegistrationEndpointFilter extends OncePerRequestFilter {
public final class OidcClientRegistrationEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for OpenID Client Registration requests.
*/
public static final String DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI = "/connect/register";
private static final String DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI = "/connect/register";
private final AuthenticationManager authenticationManager;
private final RequestMatcher clientRegistrationEndpointMatcher;
@@ -84,8 +89,22 @@ public class OidcClientRegistrationEndpointFilter extends OncePerRequestFilter {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.hasText(clientRegistrationEndpointUri, "clientRegistrationEndpointUri cannot be empty");
this.authenticationManager = authenticationManager;
this.clientRegistrationEndpointMatcher = new AntPathRequestMatcher(
clientRegistrationEndpointUri, HttpMethod.POST.name());
this.clientRegistrationEndpointMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(
clientRegistrationEndpointUri, HttpMethod.POST.name()),
createConfigureClientMatcher(clientRegistrationEndpointUri));
}
private static RequestMatcher createConfigureClientMatcher(String clientRegistrationEndpointUri) {
RequestMatcher configureClientGetMatcher = new AntPathRequestMatcher(
clientRegistrationEndpointUri, HttpMethod.GET.name());
RequestMatcher clientIdMatcher = request -> {
String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID);
return StringUtils.hasText(clientId);
};
return new AndRequestMatcher(configureClientGetMatcher, clientIdMatcher);
}
@Override
@@ -98,17 +117,17 @@ public class OidcClientRegistrationEndpointFilter extends OncePerRequestFilter {
}
try {
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
OidcClientRegistration clientRegistration = this.clientRegistrationHttpMessageConverter.read(
OidcClientRegistration.class, new ServletServerHttpRequest(request));
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication =
new OidcClientRegistrationAuthenticationToken(principal, clientRegistration);
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = convert(request);
OidcClientRegistrationAuthenticationToken clientRegistrationAuthenticationResult =
(OidcClientRegistrationAuthenticationToken) this.authenticationManager.authenticate(clientRegistrationAuthentication);
sendClientRegistrationResponse(response, clientRegistrationAuthenticationResult.getClientRegistration());
HttpStatus httpStatus = HttpStatus.OK;
if (clientRegistrationAuthentication.getClientRegistration() != null) {
httpStatus = HttpStatus.CREATED;
}
sendClientRegistrationResponse(response, httpStatus, clientRegistrationAuthenticationResult.getClientRegistration());
} catch (OAuth2AuthenticationException ex) {
sendErrorResponse(response, ex.getError());
@@ -123,18 +142,39 @@ public class OidcClientRegistrationEndpointFilter extends OncePerRequestFilter {
}
}
private void sendClientRegistrationResponse(HttpServletResponse response, OidcClientRegistration clientRegistration) throws IOException {
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);
}
private void sendClientRegistrationResponse(HttpServletResponse response, HttpStatus httpStatus, OidcClientRegistration clientRegistration) throws IOException {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.CREATED);
httpResponse.setStatusCode(httpStatus);
this.clientRegistrationHttpMessageConverter.write(clientRegistration, null, httpResponse);
}
private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_TOKEN)) {
if (OAuth2ErrorCodes.INVALID_TOKEN.equals(error.getErrorCode())) {
httpStatus = HttpStatus.UNAUTHORIZED;
} else if (error.getErrorCode().equals(OAuth2ErrorCodes.INSUFFICIENT_SCOPE)) {
} else if (OAuth2ErrorCodes.INSUFFICIENT_SCOPE.equals(error.getErrorCode())) {
httpStatus = HttpStatus.FORBIDDEN;
} else if (OAuth2ErrorCodes.INVALID_CLIENT.equals(error.getErrorCode())) {
httpStatus = HttpStatus.UNAUTHORIZED;
}
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(httpStatus);

View File

@@ -19,6 +19,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServletServerHttpResponse;
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.core.oidc.OidcProviderConfiguration;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
@@ -46,11 +47,11 @@ import java.io.IOException;
* @see ProviderSettings
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest">4.1. OpenID Provider Configuration Request</a>
*/
public class OidcProviderConfigurationEndpointFilter extends OncePerRequestFilter {
public final class OidcProviderConfigurationEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for OpenID Provider Configuration requests.
*/
public static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration";
private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration";
private final ProviderSettings providerSettings;
private final RequestMatcher requestMatcher;
@@ -76,12 +77,12 @@ public class OidcProviderConfigurationEndpointFilter extends OncePerRequestFilte
}
OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.builder()
.issuer(this.providerSettings.issuer())
.authorizationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.authorizationEndpoint()))
.tokenEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenEndpoint()))
.tokenEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
.tokenEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
.jwkSetUrl(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint()))
.issuer(this.providerSettings.getIssuer())
.authorizationEndpoint(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getAuthorizationEndpoint()))
.tokenEndpoint(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getTokenEndpoint()))
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue())
.jwkSetUrl(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getJwkSetEndpoint()))
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2020-2021 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;
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.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.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.core.oidc.http.converter.OidcUserInfoHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
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;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* A {@code Filter} that processes OpenID Connect 1.0 UserInfo Requests.
*
* @author Ido Salomon
* @author Steve Riesenberg
* @since 0.2.1
* @see OidcUserInfo
* @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 {
/**
* The default endpoint {@code URI} for OpenID Connect 1.0 UserInfo Requests.
*/
private static final String DEFAULT_OIDC_USER_INFO_ENDPOINT_URI = "/userinfo";
private final AuthenticationManager authenticationManager;
private final RequestMatcher userInfoEndpointMatcher;
private final HttpMessageConverter<OidcUserInfo> userInfoHttpMessageConverter =
new OidcUserInfoHttpMessageConverter();
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
new OAuth2ErrorHttpMessageConverter();
/**
* Constructs an {@code OidcUserInfoEndpointFilter} using the provided parameters.
*
* @param authenticationManager the authentication manager
*/
public OidcUserInfoEndpointFilter(AuthenticationManager authenticationManager) {
this(authenticationManager, DEFAULT_OIDC_USER_INFO_ENDPOINT_URI);
}
/**
* Constructs an {@code OidcUserInfoEndpointFilter} using the provided parameters.
*
* @param authenticationManager the authentication manager
* @param userInfoEndpointUri the endpoint {@code URI} for OpenID Connect 1.0 UserInfo Requests
*/
public OidcUserInfoEndpointFilter(AuthenticationManager authenticationManager, String userInfoEndpointUri) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.hasText(userInfoEndpointUri, "userInfoEndpointUri cannot be empty");
this.authenticationManager = authenticationManager;
this.userInfoEndpointMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.GET.name()),
new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.POST.name()));
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.userInfoEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
OidcUserInfoAuthenticationToken userInfoAuthentication = new OidcUserInfoAuthenticationToken(principal);
OidcUserInfoAuthenticationToken userInfoAuthenticationResult =
(OidcUserInfoAuthenticationToken) this.authenticationManager.authenticate(userInfoAuthentication);
sendUserInfoResponse(response, userInfoAuthenticationResult.getUserInfo());
} catch (OAuth2AuthenticationException ex) {
sendErrorResponse(response, ex.getError());
} 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);
} finally {
SecurityContextHolder.clearContext();
}
}
private void sendUserInfoResponse(HttpServletResponse response, OidcUserInfo userInfo) throws IOException {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
this.userInfoHttpMessageConverter.write(userInfo, null, httpResponse);
}
private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_TOKEN)) {
httpStatus = HttpStatus.UNAUTHORIZED;
} else if (error.getErrorCode().equals(OAuth2ErrorCodes.INSUFFICIENT_SCOPE)) {
httpStatus = HttpStatus.FORBIDDEN;
}
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(httpStatus);
this.errorHttpResponseConverter.write(error, null, httpResponse);
}
}

View File

@@ -45,11 +45,11 @@ import org.springframework.web.filter.OncePerRequestFilter;
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key (JWK)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517#section-5">Section 5 JWK Set Format</a>
*/
public class NimbusJwkSetEndpointFilter extends OncePerRequestFilter {
public final class NimbusJwkSetEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for JWK Set requests.
*/
public static final String DEFAULT_JWK_SET_ENDPOINT_URI = "/oauth2/jwks";
private static final String DEFAULT_JWK_SET_ENDPOINT_URI = "/oauth2/jwks";
private final JWKSource<SecurityContext> jwkSource;
private final JWKSelector jwkSelector;

View File

@@ -31,17 +31,14 @@ import org.springframework.http.MediaType;
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.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
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.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
@@ -68,6 +65,7 @@ import org.springframework.web.util.UriComponentsBuilder;
* @author Paurav Munshi
* @author Daniel Garnier-Moiroux
* @author Anoop Garlapati
* @author Dmitriy Dubson
* @since 0.0.1
* @see AuthenticationManager
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
@@ -75,11 +73,11 @@ import org.springframework.web.util.UriComponentsBuilder;
* @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>
*/
public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for authorization requests.
*/
public static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
private final AuthenticationManager authenticationManager;
private final RequestMatcher authorizationEndpointMatcher;
@@ -89,33 +87,6 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
private String consentPage;
/**
* Constructs an {@code OAuth2AuthorizationEndpointFilter} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
* @deprecated use {@link #OAuth2AuthorizationEndpointFilter(AuthenticationManager)} instead.
*/
@Deprecated
public OAuth2AuthorizationEndpointFilter(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService) {
this(null);
}
/**
* Constructs an {@code OAuth2AuthorizationEndpointFilter} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
* @param authorizationEndpointUri the endpoint {@code URI} for authorization requests
* @deprecated use {@link #OAuth2AuthorizationEndpointFilter(AuthenticationManager, String)} instead.
*/
@Deprecated
public OAuth2AuthorizationEndpointFilter(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService, String authorizationEndpointUri) {
this(null, authorizationEndpointUri);
}
/**
* Constructs an {@code OAuth2AuthorizationEndpointFilter} using the provided parameters.
*
@@ -194,7 +165,6 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
request, response, authorizationCodeRequestAuthenticationResult);
} catch (OAuth2AuthenticationException ex) {
SecurityContextHolder.clearContext();
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}
}
@@ -205,7 +175,7 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
*
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
*/
public final void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
}
@@ -216,7 +186,7 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
*
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
*/
public final void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
}
@@ -227,7 +197,7 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
*
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
*/
public final void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
this.authenticationFailureHandler = authenticationFailureHandler;
}
@@ -238,7 +208,7 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
*
* @param consentPage the URI of the custom consent page to redirect to if consent is required (e.g. "/oauth2/consent")
*/
public final void setConsentPage(String consentPage) {
public void setConsentPage(String consentPage) {
this.consentPage = consentPage;
}
@@ -363,6 +333,12 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
builder.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">");
builder.append(" <link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css\" integrity=\"sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z\" crossorigin=\"anonymous\">");
builder.append(" <title>Consent required</title>");
builder.append(" <script>");
builder.append(" function cancelConsent() {");
builder.append(" document.consent_form.reset();");
builder.append(" document.consent_form.submit();");
builder.append(" }");
builder.append(" </script>");
builder.append("</head>");
builder.append("<body>");
builder.append("<div class=\"container\">");
@@ -381,7 +357,7 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
builder.append(" </div>");
builder.append(" <div class=\"row\">");
builder.append(" <div class=\"col text-center\">");
builder.append(" <form method=\"post\" action=\"" + request.getRequestURI() + "\">");
builder.append(" <form name=\"consent_form\" method=\"post\" action=\"" + request.getRequestURI() + "\">");
builder.append(" <input type=\"hidden\" name=\"client_id\" value=\"" + clientId + "\">");
builder.append(" <input type=\"hidden\" name=\"state\" value=\"" + state + "\">");
@@ -403,10 +379,10 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
}
builder.append(" <div class=\"form-group pt-3\">");
builder.append(" <button class=\"btn btn-primary btn-lg\" type=\"submit\">Submit Consent</button>");
builder.append(" <button class=\"btn btn-primary btn-lg\" type=\"submit\" id=\"submit-consent\">Submit Consent</button>");
builder.append(" </div>");
builder.append(" <div class=\"form-group\">");
builder.append(" <button class=\"btn btn-link regular\" type=\"reset\">Cancel</button>");
builder.append(" <button class=\"btn btn-link regular\" type=\"button\" onclick=\"cancelConsent();\" id=\"cancel-consent\">Cancel</button>");
builder.append(" </div>");
builder.append(" </form>");
builder.append(" </div>");

View File

@@ -28,6 +28,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadata;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.http.converter.OAuth2AuthorizationServerMetadataHttpMessageConverter;
@@ -47,11 +48,11 @@ import org.springframework.web.util.UriComponentsBuilder;
* @see ProviderSettings
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3">3. Obtaining Authorization Server Metadata</a>
*/
public class OAuth2AuthorizationServerMetadataEndpointFilter extends OncePerRequestFilter {
public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for OAuth 2.0 Authorization Server Metadata requests.
*/
public static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server";
private static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server";
private final ProviderSettings providerSettings;
private final RequestMatcher requestMatcher;
@@ -77,18 +78,18 @@ public class OAuth2AuthorizationServerMetadataEndpointFilter extends OncePerRequ
}
OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder()
.issuer(this.providerSettings.issuer())
.authorizationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.authorizationEndpoint()))
.tokenEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenEndpoint()))
.issuer(this.providerSettings.getIssuer())
.authorizationEndpoint(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getAuthorizationEndpoint()))
.tokenEndpoint(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getTokenEndpoint()))
.tokenEndpointAuthenticationMethods(clientAuthenticationMethods())
.jwkSetUrl(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint()))
.jwkSetUrl(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getJwkSetEndpoint()))
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
.tokenRevocationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenRevocationEndpoint()))
.tokenRevocationEndpoint(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getTokenRevocationEndpoint()))
.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
.tokenIntrospectionEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenIntrospectionEndpoint()))
.tokenIntrospectionEndpoint(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getTokenIntrospectionEndpoint()))
.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
.codeChallengeMethod("plain")
.codeChallengeMethod("S256")
@@ -101,8 +102,8 @@ public class OAuth2AuthorizationServerMetadataEndpointFilter extends OncePerRequ
private static Consumer<List<String>> clientAuthenticationMethods() {
return (authenticationMethods) -> {
authenticationMethods.add("client_secret_basic"); // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
authenticationMethods.add("client_secret_post"); // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue());
};
}

View File

@@ -39,6 +39,10 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
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.PublicClientAuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
@@ -55,18 +59,18 @@ import org.springframework.web.filter.OncePerRequestFilter;
* @since 0.0.1
* @see AuthenticationManager
* @see OAuth2ClientAuthenticationProvider
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-2.3">Section 2.3 Client Authentication</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.2.1">Section 3.2.1 Token Endpoint Client Authentication</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-2.3">Section 2.3 Client Authentication</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1">Section 3.2.1 Token Endpoint Client Authentication</a>
*/
public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter {
public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter {
private final AuthenticationManager authenticationManager;
private final RequestMatcher requestMatcher;
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource =
new WebAuthenticationDetailsSource();
private AuthenticationConverter authenticationConverter;
private AuthenticationSuccessHandler authenticationSuccessHandler;
private AuthenticationFailureHandler authenticationFailureHandler;
private AuthenticationSuccessHandler authenticationSuccessHandler = this::onAuthenticationSuccess;
private AuthenticationFailureHandler authenticationFailureHandler = this::onAuthenticationFailure;
/**
* Constructs an {@code OAuth2ClientAuthenticationFilter} using the provided parameters.
@@ -85,59 +89,63 @@ public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter {
new ClientSecretBasicAuthenticationConverter(),
new ClientSecretPostAuthenticationConverter(),
new PublicClientAuthenticationConverter()));
this.authenticationSuccessHandler = this::onAuthenticationSuccess;
this.authenticationFailureHandler = this::onAuthenticationFailure;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (this.requestMatcher.matches(request)) {
try {
Authentication authenticationRequest = this.authenticationConverter.convert(request);
if (authenticationRequest instanceof AbstractAuthenticationToken) {
((AbstractAuthenticationToken) authenticationRequest).setDetails(
this.authenticationDetailsSource.buildDetails(request));
}
if (authenticationRequest != null) {
Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
}
} catch (OAuth2AuthenticationException failed) {
this.authenticationFailureHandler.onAuthenticationFailure(request, response, failed);
return;
}
if (!this.requestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
Authentication authenticationRequest = this.authenticationConverter.convert(request);
if (authenticationRequest instanceof AbstractAuthenticationToken) {
((AbstractAuthenticationToken) authenticationRequest).setDetails(
this.authenticationDetailsSource.buildDetails(request));
}
if (authenticationRequest != null) {
Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
}
filterChain.doFilter(request, response);
} catch (OAuth2AuthenticationException ex) {
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}
filterChain.doFilter(request, response);
}
/**
* Sets the {@link AuthenticationConverter} used for converting a {@link HttpServletRequest} to an {@link OAuth2ClientAuthenticationToken}.
* Sets the {@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 used for converting a {@link HttpServletRequest} to an {@link OAuth2ClientAuthenticationToken}
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
*/
public final void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling successful authentications.
* Sets the {@link AuthenticationSuccessHandler} used for handling a successful client authentication
* and associating the {@link OAuth2ClientAuthenticationToken} to the {@link SecurityContext}.
*
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling successful authentications
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling a successful client authentication
*/
public final void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling failed authentications.
* Sets the {@link AuthenticationFailureHandler} used for handling a failed client authentication
* and returning the {@link OAuth2Error Error Response}.
*
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling failed authentications
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling a failed client authentication
*/
public final void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
this.authenticationFailureHandler = authenticationFailureHandler;
}
@@ -145,13 +153,13 @@ public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter {
private void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
SecurityContextHolder.setContext(securityContext);
}
private void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException {
AuthenticationException exception) throws IOException {
SecurityContextHolder.clearContext();
@@ -163,13 +171,16 @@ public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter {
// include the "WWW-Authenticate" response header field
// matching the authentication scheme used by the client.
OAuth2Error error = ((OAuth2AuthenticationException) failed).getError();
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
if (OAuth2ErrorCodes.INVALID_CLIENT.equals(error.getErrorCode())) {
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
} else {
httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
}
this.errorHttpResponseConverter.write(error, null, httpResponse);
// We don't want to reveal too much information to the caller so just return the error code
OAuth2Error errorResponse = new OAuth2Error(error.getErrorCode());
this.errorHttpResponseConverter.write(errorResponse, null, httpResponse);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 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,22 +15,18 @@
*/
package org.springframework.security.oauth2.server.authorization.web;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Utility methods for the OAuth 2.0 Protocol Endpoints.
*
* @author Joe Grandja
* @since 0.0.1
* @see OAuth2AuthorizationEndpointFilter
* @see OAuth2TokenEndpointFilter
*/
final class OAuth2EndpointUtils {
@@ -50,10 +46,4 @@ final class OAuth2EndpointUtils {
return parameters;
}
static boolean matchesPkceTokenRequest(HttpServletRequest request) {
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) &&
request.getParameter(OAuth2ParameterNames.CODE) != null &&
request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
}
}

View File

@@ -48,6 +48,7 @@ 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.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;
@@ -90,11 +91,11 @@ import org.springframework.web.filter.OncePerRequestFilter;
* @see OAuth2ClientCredentialsAuthenticationProvider
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.2">Section 3.2 Token Endpoint</a>
*/
public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
public final class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for access token requests.
*/
public static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private final AuthenticationManager authenticationManager;
@@ -103,7 +104,7 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
new OAuth2AccessTokenResponseHttpMessageConverter();
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
new OAuth2ErrorHttpMessageConverter();
private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource =
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource =
new WebAuthenticationDetailsSource();
private AuthenticationConverter authenticationConverter;
private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendAccessTokenResponse;
@@ -169,13 +170,23 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
}
}
/**
* Sets the {@link AuthenticationDetailsSource} used for building an authentication details instance from {@link HttpServletRequest}.
*
* @param authenticationDetailsSource the {@link AuthenticationDetailsSource} used for building an authentication details instance from {@link HttpServletRequest}
*/
public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
this.authenticationDetailsSource = authenticationDetailsSource;
}
/**
* Sets the {@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 authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest}
*/
public final void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
}
@@ -186,7 +197,7 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
*
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AccessTokenAuthenticationToken}
*/
public final void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
}
@@ -197,7 +208,7 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
*
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
*/
public final void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
this.authenticationFailureHandler = authenticationFailureHandler;
}

View File

@@ -16,8 +16,8 @@
package org.springframework.security.oauth2.server.authorization.web;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@@ -36,7 +36,7 @@ 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.OAuth2TokenIntrospection;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
@@ -58,11 +58,11 @@ import org.springframework.web.filter.OncePerRequestFilter;
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.1">Section 2.1 Introspection Request</a>
* @since 0.1.1
*/
public class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequestFilter {
public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for token introspection requests.
*/
public static final String DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI = "/oauth2/introspect";
private static final String DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI = "/oauth2/introspect";
private final AuthenticationManager authenticationManager;
private final RequestMatcher tokenIntrospectionEndpointMatcher;
@@ -148,27 +148,26 @@ public class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequestFilter
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// token (REQUIRED)
String token = parameters.getFirst(OAuth2ParameterNames2.TOKEN);
String token = parameters.getFirst(OAuth2ParameterNames.TOKEN);
if (!StringUtils.hasText(token) ||
parameters.get(OAuth2ParameterNames2.TOKEN).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames2.TOKEN);
parameters.get(OAuth2ParameterNames.TOKEN).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN);
}
// token_type_hint (OPTIONAL)
String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames2.TOKEN_TYPE_HINT);
String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames.TOKEN_TYPE_HINT);
if (StringUtils.hasText(tokenTypeHint) &&
parameters.get(OAuth2ParameterNames2.TOKEN_TYPE_HINT).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames2.TOKEN_TYPE_HINT);
parameters.get(OAuth2ParameterNames.TOKEN_TYPE_HINT).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN_TYPE_HINT);
}
// @formatter:off
Map<String, Object> additionalParameters = parameters
.entrySet()
.stream()
.filter(e -> !e.getKey().equals(OAuth2ParameterNames2.TOKEN) &&
!e.getKey().equals(OAuth2ParameterNames2.TOKEN_TYPE_HINT))
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
// @formatter:on
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);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 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,6 +15,13 @@
*/
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.core.convert.converter.Converter;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
@@ -26,7 +33,7 @@ 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.OAuth2ParameterNames2;
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;
@@ -37,12 +44,6 @@ import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* A {@code Filter} for the OAuth 2.0 Token Revocation endpoint.
*
@@ -53,11 +54,11 @@ import java.io.IOException;
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7009#section-2.1">Section 2.1 Revocation Request</a>
* @since 0.0.3
*/
public class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFilter {
public final class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for token revocation requests.
*/
public static final String DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI = "/oauth2/revoke";
private static final String DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI = "/oauth2/revoke";
private final AuthenticationManager authenticationManager;
private final RequestMatcher tokenRevocationEndpointMatcher;
@@ -131,17 +132,17 @@ public class OAuth2TokenRevocationEndpointFilter extends OncePerRequestFilter {
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// token (REQUIRED)
String token = parameters.getFirst(OAuth2ParameterNames2.TOKEN);
String token = parameters.getFirst(OAuth2ParameterNames.TOKEN);
if (!StringUtils.hasText(token) ||
parameters.get(OAuth2ParameterNames2.TOKEN).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames2.TOKEN);
parameters.get(OAuth2ParameterNames.TOKEN).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN);
}
// token_type_hint (OPTIONAL)
String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames2.TOKEN_TYPE_HINT);
String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames.TOKEN_TYPE_HINT);
if (StringUtils.hasText(tokenTypeHint) &&
parameters.get(OAuth2ParameterNames2.TOKEN_TYPE_HINT).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames2.TOKEN_TYPE_HINT);
parameters.get(OAuth2ParameterNames.TOKEN_TYPE_HINT).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.TOKEN_TYPE_HINT);
}
return new OAuth2TokenRevocationAuthenticationToken(token, clientPrincipal, tokenTypeHint);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 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.
@@ -13,19 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web;
package org.springframework.security.oauth2.server.authorization.web.authentication;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
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.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@@ -33,6 +22,20 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
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.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.StringUtils;
/**
* Attempts to extract HTTP Basic credentials from {@link HttpServletRequest}
* and then converts to an {@link OAuth2ClientAuthenticationToken} used for authenticating the client.
@@ -40,11 +43,13 @@ import java.util.Map;
* @author Patryk Kostrzewa
* @author Joe Grandja
* @since 0.0.1
* @see AuthenticationConverter
* @see OAuth2ClientAuthenticationToken
* @see OAuth2ClientAuthenticationFilter
*/
public class ClientSecretBasicAuthenticationConverter implements AuthenticationConverter {
public final class ClientSecretBasicAuthenticationConverter implements AuthenticationConverter {
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
@@ -58,7 +63,7 @@ public class ClientSecretBasicAuthenticationConverter implements AuthenticationC
}
if (parts.length != 2) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
byte[] decodedCredentials;
@@ -74,7 +79,7 @@ public class ClientSecretBasicAuthenticationConverter implements AuthenticationC
if (credentials.length != 2 ||
!StringUtils.hasText(credentials[0]) ||
!StringUtils.hasText(credentials[1])) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
String clientID;
@@ -86,16 +91,17 @@ public class ClientSecretBasicAuthenticationConverter implements AuthenticationC
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);
}
return new OAuth2ClientAuthenticationToken(clientID, clientSecret, ClientAuthenticationMethod.BASIC,
return new OAuth2ClientAuthenticationToken(clientID, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, clientSecret,
extractAdditionalParameters(request));
}
private static Map<String, Object> extractAdditionalParameters(HttpServletRequest request) {
Map<String, Object> additionalParameters = Collections.emptyMap();
if (OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {
if (OAuth2EndpointUtils.matchesAuthorizationCodeGrantRequest(request)) {
// Confidential clients can also leverage PKCE
additionalParameters = new HashMap<>(OAuth2EndpointUtils.getParameters(request).toSingleValueMap());
}
return additionalParameters;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 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.
@@ -13,36 +13,40 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web;
package org.springframework.security.oauth2.server.authorization.web.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
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.OAuth2ClientAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
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.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Attempts to extract client credentials from POST parameters of {@link HttpServletRequest}
* and then converts to an {@link OAuth2ClientAuthenticationToken} used for authenticating the client.
*
* @author Anoop Garlapati
* @since 0.1.0
* @see AuthenticationConverter
* @see OAuth2ClientAuthenticationToken
* @see OAuth2ClientAuthenticationFilter
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-2.3.1">Section 2.3.1 Client Password</a>
*/
public class ClientSecretPostAuthenticationConverter implements AuthenticationConverter {
public final class ClientSecretPostAuthenticationConverter implements AuthenticationConverter {
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
@@ -54,7 +58,7 @@ public class ClientSecretPostAuthenticationConverter implements AuthenticationCo
}
if (parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
// client_secret (REQUIRED)
@@ -64,16 +68,16 @@ public class ClientSecretPostAuthenticationConverter implements AuthenticationCo
}
if (parameters.get(OAuth2ParameterNames.CLIENT_SECRET).size() != 1) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
return new OAuth2ClientAuthenticationToken(clientId, clientSecret, ClientAuthenticationMethod.POST,
return new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.CLIENT_SECRET_POST, clientSecret,
extractAdditionalParameters(request));
}
private static Map<String, Object> extractAdditionalParameters(HttpServletRequest request) {
Map<String, Object> additionalParameters = Collections.emptyMap();
if (OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {
if (OAuth2EndpointUtils.matchesAuthorizationCodeGrantRequest(request)) {
// Confidential clients can also leverage PKCE
additionalParameters = new HashMap<>(OAuth2EndpointUtils.getParameters(request).toSingleValueMap());
additionalParameters.remove(OAuth2ParameterNames.CLIENT_ID);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 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.
@@ -13,19 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web;
package org.springframework.security.oauth2.server.authorization.web.authentication;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
/**
* An {@link AuthenticationConverter} that simply delegates to it's
* internal {@code List} of {@link AuthenticationConverter}(s).
@@ -55,12 +55,12 @@ public final class DelegatingAuthenticationConverter implements AuthenticationCo
@Override
public Authentication convert(HttpServletRequest request) {
Assert.notNull(request, "request cannot be null");
// @formatter:off
return this.converters.stream()
.map(converter -> converter.convert(request))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
// @formatter:on
for (AuthenticationConverter converter : this.converters) {
Authentication authentication = converter.convert(request);
if (authentication != null) {
return authentication;
}
}
return null;
}
}

View File

@@ -15,8 +15,8 @@
*/
package org.springframework.security.oauth2.server.authorization.web.authentication;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
@@ -78,16 +78,15 @@ public final class OAuth2AuthorizationCodeAuthenticationConverter implements Aut
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// @formatter:off
Map<String, Object> additionalParameters = parameters
.entrySet()
.stream()
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
!e.getKey().equals(OAuth2ParameterNames.CLIENT_ID) &&
!e.getKey().equals(OAuth2ParameterNames.CODE) &&
!e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI))
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
// @formatter:on
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.CODE) &&
!key.equals(OAuth2ParameterNames.REDIRECT_URI)) {
additionalParameters.put(key, value.get(0));
}
});
return new OAuth2AuthorizationCodeAuthenticationToken(
code, clientPrincipal, redirectUri, additionalParameters);

View File

@@ -16,10 +16,10 @@
package org.springframework.security.oauth2.server.authorization.web.authentication;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
@@ -118,11 +118,20 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
}
}
// state (RECOMMENDED)
// state
// RECOMMENDED for Authorization Request
String state = parameters.getFirst(OAuth2ParameterNames.STATE);
if (StringUtils.hasText(state) &&
parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, 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);
}
}
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
@@ -139,17 +148,16 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI);
}
// @formatter:off
Map<String, Object> additionalParameters = parameters
.entrySet()
.stream()
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.RESPONSE_TYPE) &&
!e.getKey().equals(OAuth2ParameterNames.CLIENT_ID) &&
!e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI) &&
!e.getKey().equals(OAuth2ParameterNames.SCOPE) &&
!e.getKey().equals(OAuth2ParameterNames.STATE))
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
// @formatter:on
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.RESPONSE_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.REDIRECT_URI) &&
!key.equals(OAuth2ParameterNames.SCOPE) &&
!key.equals(OAuth2ParameterNames.STATE)) {
additionalParameters.put(key, value.get(0));
}
});
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, principal)
.authorizationUri(authorizationUri)

View File

@@ -16,10 +16,10 @@
package org.springframework.security.oauth2.server.authorization.web.authentication;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
@@ -75,14 +75,13 @@ public final class OAuth2ClientCredentialsAuthenticationConverter implements Aut
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
// @formatter:off
Map<String, Object> additionalParameters = parameters
.entrySet()
.stream()
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
!e.getKey().equals(OAuth2ParameterNames.SCOPE))
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
// @formatter:on
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, value.get(0));
}
});
return new OAuth2ClientCredentialsAuthenticationToken(
clientPrincipal, requestedScopes, additionalParameters);

View File

@@ -19,8 +19,11 @@ import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -49,6 +52,17 @@ final class OAuth2EndpointUtils {
return parameters;
}
static boolean matchesAuthorizationCodeGrantRequest(HttpServletRequest request) {
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) &&
request.getParameter(OAuth2ParameterNames.CODE) != null;
}
static boolean matchesPkceTokenRequest(HttpServletRequest request) {
return matchesAuthorizationCodeGrantRequest(request) &&
request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
}
static void throwError(String errorCode, String parameterName, String errorUri) {
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throw new OAuth2AuthenticationException(error);

View File

@@ -16,10 +16,10 @@
package org.springframework.security.oauth2.server.authorization.web.authentication;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
@@ -85,15 +85,14 @@ public final class OAuth2RefreshTokenAuthenticationConverter implements Authenti
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
// @formatter:off
Map<String, Object> additionalParameters = parameters
.entrySet()
.stream()
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
!e.getKey().equals(OAuth2ParameterNames.REFRESH_TOKEN) &&
!e.getKey().equals(OAuth2ParameterNames.SCOPE))
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
// @formatter:on
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.REFRESH_TOKEN) &&
!key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, value.get(0));
}
});
return new OAuth2RefreshTokenAuthenticationToken(
refreshToken, clientPrincipal, requestedScopes, additionalParameters);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 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.
@@ -13,22 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web;
package org.springframework.security.oauth2.server.authorization.web.authentication;
import java.util.HashMap;
import javax.servlet.http.HttpServletRequest;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
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.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
/**
* Attempts to extract the parameters from {@link HttpServletRequest}
* used for authenticating public clients using Proof Key for Code Exchange (PKCE).
@@ -40,8 +43,9 @@ import java.util.HashMap;
* @see OAuth2ClientAuthenticationFilter
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636">Proof Key for Code Exchange by OAuth Public Clients</a>
*/
public class PublicClientAuthenticationConverter implements AuthenticationConverter {
public final class PublicClientAuthenticationConverter implements AuthenticationConverter {
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
if (!OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {
@@ -54,17 +58,17 @@ public class PublicClientAuthenticationConverter implements AuthenticationConver
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
if (!StringUtils.hasText(clientId) ||
parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
// code_verifier (REQUIRED)
if (parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
parameters.remove(OAuth2ParameterNames.CLIENT_ID);
return new OAuth2ClientAuthenticationToken(
clientId, new HashMap<>(parameters.toSingleValueMap()));
return new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.NONE, null,
new HashMap<>(parameters.toSingleValueMap()));
}
}

View File

@@ -3,7 +3,7 @@ CREATE TABLE oauth2_authorization (
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorization_grant_type varchar(100) NOT NULL,
attributes varchar(4000) DEFAULT NULL,
attributes varchar(15000) DEFAULT NULL,
state varchar(500) DEFAULT NULL,
authorization_code_value blob DEFAULT NULL,
authorization_code_issued_at timestamp DEFAULT NULL,

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configuration;
import java.util.function.Supplier;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.endsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link RegisterMissingBeanPostProcessor}.
*
* @author Steve Riesenberg
*/
public class RegisterMissingBeanPostProcessorTests {
private final RegisterMissingBeanPostProcessor postProcessor = new RegisterMissingBeanPostProcessor();
@Test
public void postProcessBeanDefinitionRegistryWhenClassAddedThenRegisteredWithClass() {
this.postProcessor.addBeanDefinition(SimpleBean.class, null);
this.postProcessor.setBeanFactory(new DefaultListableBeanFactory());
BeanDefinitionRegistry beanDefinitionRegistry = mock(BeanDefinitionRegistry.class);
this.postProcessor.postProcessBeanDefinitionRegistry(beanDefinitionRegistry);
ArgumentCaptor<BeanDefinition> beanDefinitionCaptor = ArgumentCaptor.forClass(BeanDefinition.class);
verify(beanDefinitionRegistry).registerBeanDefinition(endsWith("SimpleBean"), beanDefinitionCaptor.capture());
RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionCaptor.getValue();
assertThat(beanDefinition.getBeanClass()).isEqualTo(SimpleBean.class);
assertThat(beanDefinition.getInstanceSupplier()).isNull();
}
@Test
public void postProcessBeanDefinitionRegistryWhenSupplierAddedThenRegisteredWithSupplier() {
Supplier<SimpleBean> beanSupplier = () -> new SimpleBean("string");
this.postProcessor.addBeanDefinition(SimpleBean.class, beanSupplier);
this.postProcessor.setBeanFactory(new DefaultListableBeanFactory());
BeanDefinitionRegistry beanDefinitionRegistry = mock(BeanDefinitionRegistry.class);
this.postProcessor.postProcessBeanDefinitionRegistry(beanDefinitionRegistry);
ArgumentCaptor<BeanDefinition> beanDefinitionCaptor = ArgumentCaptor.forClass(BeanDefinition.class);
verify(beanDefinitionRegistry).registerBeanDefinition(endsWith("SimpleBean"), beanDefinitionCaptor.capture());
RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionCaptor.getValue();
assertThat(beanDefinition.getBeanClass()).isEqualTo(SimpleBean.class);
assertThat(beanDefinition.getInstanceSupplier()).isEqualTo(beanSupplier);
}
@Test
public void postProcessBeanDefinitionRegistryWhenNoBeanDefinitionsAddedThenNoneRegistered() {
this.postProcessor.setBeanFactory(new DefaultListableBeanFactory());
BeanDefinitionRegistry beanDefinitionRegistry = mock(BeanDefinitionRegistry.class);
this.postProcessor.postProcessBeanDefinitionRegistry(beanDefinitionRegistry);
verifyNoInteractions(beanDefinitionRegistry);
}
@Test
public void postProcessBeanDefinitionRegistryWhenBeanDefinitionAlreadyExistsThenNoneRegistered() {
this.postProcessor.addBeanDefinition(SimpleBean.class, null);
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerBeanDefinition("simpleBean", new RootBeanDefinition(SimpleBean.class));
this.postProcessor.setBeanFactory(beanFactory);
BeanDefinitionRegistry beanDefinitionRegistry = mock(BeanDefinitionRegistry.class);
this.postProcessor.postProcessBeanDefinitionRegistry(beanDefinitionRegistry);
verifyNoInteractions(beanDefinitionRegistry);
}
private static final class SimpleBean {
private final String field;
private SimpleBean(String field) {
this.field = field;
}
private String getField() {
return field;
}
}
}

View File

@@ -44,7 +44,6 @@ import org.springframework.security.oauth2.server.authorization.client.JdbcRegis
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.CoreMatchers.containsString;
@@ -59,6 +58,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* @author Florian Berthe
*/
public class JwkSetTests {
private static final String DEFAULT_JWK_SET_ENDPOINT_URI = "/oauth2/jwks";
private static EmbeddedDatabase db;
private static JWKSource<SecurityContext> jwkSource;
private static ProviderSettings providerSettings;
@@ -76,7 +76,7 @@ public class JwkSetTests {
public static void init() {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
providerSettings = new ProviderSettings().jwkSetEndpoint("/test/jwks");
providerSettings = ProviderSettings.builder().jwkSetEndpoint("/test/jwks").build();
db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
@@ -101,14 +101,14 @@ public class JwkSetTests {
public void requestWhenJwkSetThenReturnKeys() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
assertJwkSetRequestThenReturnKeys(NimbusJwkSetEndpointFilter.DEFAULT_JWK_SET_ENDPOINT_URI);
assertJwkSetRequestThenReturnKeys(DEFAULT_JWK_SET_ENDPOINT_URI);
}
@Test
public void requestWhenJwkSetCustomEndpointThenReturnKeys() throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomEndpoints.class).autowire();
assertJwkSetRequestThenReturnKeys(providerSettings.jwkSetEndpoint());
assertJwkSetRequestThenReturnKeys(providerSettings.getJwkSetEndpoint());
}
private void assertJwkSetRequestThenReturnKeys(String jwkSetEndpointUri) throws Exception {

View File

@@ -23,14 +23,18 @@ import java.security.Principal;
import java.text.MessageFormat;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.function.Consumer;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.assertj.core.matcher.AssertionMatcher;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -58,9 +62,11 @@ import org.springframework.security.config.annotation.web.configuration.OAuth2Au
import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
@@ -76,20 +82,22 @@ import org.springframework.security.oauth2.server.authorization.JdbcOAuth2Author
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
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.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
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.OAuth2AuthorizationConsentAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
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.config.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
@@ -122,8 +130,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
*
* @author Joe Grandja
* @author Daniel Garnier-Moiroux
* @author Dmitriy Dubson
* @author Steve Riesenberg
*/
public class OAuth2AuthorizationCodeGrantTests {
private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
// See RFC 7636: Appendix B. Example for the S256 code_challenge_method
// https://tools.ietf.org/html/rfc7636#appendix-B
private static final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
@@ -167,9 +179,10 @@ public class OAuth2AuthorizationCodeGrantTests {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
jwtEncoder = new NimbusJwsEncoder(jwkSource);
providerSettings = new ProviderSettings()
providerSettings = ProviderSettings.builder()
.authorizationEndpoint("/test/authorize")
.tokenEndpoint("/test/token");
.tokenEndpoint("/test/token")
.build();
authorizationRequestConverter = mock(AuthenticationConverter.class);
authorizationRequestAuthenticationProvider = mock(AuthenticationProvider.class);
authorizationResponseHandler = mock(AuthenticationSuccessHandler.class);
@@ -203,7 +216,7 @@ public class OAuth2AuthorizationCodeGrantTests {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
this.registeredClientRepository.save(registeredClient);
this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.params(getAuthorizationRequestParameters(registeredClient)))
.andExpect(status().isUnauthorized())
.andReturn();
@@ -215,7 +228,7 @@ public class OAuth2AuthorizationCodeGrantTests {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.params(getAuthorizationRequestParameters(registeredClient)))
.andExpect(status().isBadRequest())
.andReturn();
@@ -225,14 +238,14 @@ public class OAuth2AuthorizationCodeGrantTests {
public void requestWhenAuthorizationRequestAuthenticatedThenRedirectToClient() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
assertAuthorizationRequestRedirectsToClient(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI);
assertAuthorizationRequestRedirectsToClient(DEFAULT_AUTHORIZATION_ENDPOINT_URI);
}
@Test
public void requestWhenAuthorizationRequestCustomEndpointThenRedirectToClient() throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomEndpoints.class).autowire();
assertAuthorizationRequestRedirectsToClient(providerSettings.authorizationEndpoint());
assertAuthorizationRequestRedirectsToClient(providerSettings.getAuthorizationEndpoint());
}
private void assertAuthorizationRequestRedirectsToClient(String authorizationEndpointUri) throws Exception {
@@ -264,15 +277,17 @@ public class OAuth2AuthorizationCodeGrantTests {
this.authorizationService.save(authorization);
OAuth2AccessTokenResponse accessTokenResponse = assertTokenRequestReturnsAccessTokenResponse(
registeredClient, authorization, OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI);
registeredClient, authorization, DEFAULT_TOKEN_ENDPOINT_URI);
// Assert user authorities was propagated as claim in JWT
Jwt jwt = this.jwtDecoder.decode(accessTokenResponse.getAccessToken().getTokenValue());
List<String> authoritiesClaim = jwt.getClaim(AUTHORITIES_CLAIM);
Authentication principal = authorization.getAttribute(Principal.class.getName());
Set<String> userAuthorities = principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
Set<String> userAuthorities = new HashSet<>();
for (GrantedAuthority authority : principal.getAuthorities()) {
userAuthorities.add(authority.getAuthority());
}
assertThat(authoritiesClaim).containsExactlyInAnyOrderElementsOf(userAuthorities);
}
@@ -287,7 +302,7 @@ public class OAuth2AuthorizationCodeGrantTests {
this.authorizationService.save(authorization);
assertTokenRequestReturnsAccessTokenResponse(
registeredClient, authorization, providerSettings.tokenEndpoint());
registeredClient, authorization, providerSettings.getTokenEndpoint());
}
private OAuth2AccessTokenResponse assertTokenRequestReturnsAccessTokenResponse(RegisteredClient registeredClient,
@@ -327,7 +342,7 @@ public class OAuth2AuthorizationCodeGrantTests {
RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build();
this.registeredClientRepository.save(registeredClient);
MvcResult mvcResult = this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.params(getAuthorizationRequestParameters(registeredClient))
.param(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE)
.param(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")
@@ -342,7 +357,7 @@ public class OAuth2AuthorizationCodeGrantTests {
assertThat(authorizationCodeAuthorization).isNotNull();
assertThat(authorizationCodeAuthorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER))
@@ -364,6 +379,35 @@ public class OAuth2AuthorizationCodeGrantTests {
assertThat(authorizationCodeToken.getMetadata().get(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME)).isEqualTo(true);
}
@Test
public void requestWhenConfidentialClientWithPkceAndMissingCodeVerifierThenUnauthorized() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
this.registeredClientRepository.save(registeredClient);
MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.params(getAuthorizationRequestParameters(registeredClient))
.param(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE)
.param(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")
.with(user("user")))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=state");
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
assertThat(authorizationCodeAuthorization).isNotNull();
assertThat(authorizationCodeAuthorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
.andExpect(status().isUnauthorized());
}
@Test
public void requestWhenCustomJwtEncoderThenUsed() throws Exception {
this.spring.register(AuthorizationServerConfigurationWithJwtEncoder.class).autowire();
@@ -374,7 +418,7 @@ public class OAuth2AuthorizationCodeGrantTests {
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
this.authorizationService.save(authorization);
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(getTokenRequestParameters(registeredClient, authorization))
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)));
}
@@ -389,11 +433,11 @@ public class OAuth2AuthorizationCodeGrantTests {
scopes.add("message.read");
scopes.add("message.write");
})
.clientSettings(settings -> settings.requireUserConsent(true))
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
this.registeredClientRepository.save(registeredClient);
String consentPage = this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
String consentPage = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.params(getAuthorizationRequestParameters(registeredClient))
.with(user("user")))
.andExpect(status().is2xxSuccessful())
@@ -416,7 +460,7 @@ public class OAuth2AuthorizationCodeGrantTests {
scopes.add("message.read");
scopes.add("message.write");
})
.clientSettings(settings -> settings.requireUserConsent(true))
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
this.registeredClientRepository.save(registeredClient);
@@ -425,7 +469,7 @@ public class OAuth2AuthorizationCodeGrantTests {
.build();
this.authorizationService.save(authorization);
MvcResult mvcResult = this.mvc.perform(post(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
MvcResult mvcResult = this.mvc.perform(post(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
.param(OAuth2ParameterNames.SCOPE, "message.read")
.param(OAuth2ParameterNames.SCOPE, "message.write")
@@ -440,7 +484,7 @@ public class OAuth2AuthorizationCodeGrantTests {
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
.andExpect(status().isOk())
@@ -464,11 +508,11 @@ public class OAuth2AuthorizationCodeGrantTests {
scopes.add("message.read");
scopes.add("message.write");
})
.clientSettings(settings -> settings.requireUserConsent(true))
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
this.registeredClientRepository.save(registeredClient);
MvcResult mvcResult = this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.params(getAuthorizationRequestParameters(registeredClient))
.with(user("user")))
.andExpect(status().is3xxRedirection())
@@ -489,6 +533,58 @@ public class OAuth2AuthorizationCodeGrantTests {
assertThat(authorization).isNotNull();
}
@Test
public void requestWhenCustomConsentCustomizerConfiguredThenUsed() throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomConsentRequest.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true)
.setting("custom.allowed-authorities", "authority-1 authority-2")
.build())
.build();
this.registeredClientRepository.save(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.build();
this.authorizationService.save(authorization);
MvcResult mvcResult = this.mvc.perform(post(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
.param("authority", "authority-1 authority-2")
.param(OAuth2ParameterNames.STATE, "state")
.with(user("principal")))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=state");
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.access_token").value(new AssertionMatcher<String>() {
@Override
public void assertion(String accessToken) throws AssertionError {
Jwt jwt = jwtDecoder.decode(accessToken);
assertThat(jwt.getClaimAsStringList(AUTHORITIES_CLAIM))
.containsExactlyInAnyOrder("authority-1", "authority-2");
}
}))
.andExpect(jsonPath("$.token_type").isNotEmpty())
.andExpect(jsonPath("$.expires_in").isNotEmpty())
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
.andExpect(jsonPath("$.scope").doesNotExist())
.andReturn();
}
@Test
public void requestWhenAuthorizationEndpointCustomizedThenUsed() throws Exception {
this.spring.register(AuthorizationServerConfigurationCustomAuthorizationEndpoint.class).autowire();
@@ -509,7 +605,7 @@ public class OAuth2AuthorizationCodeGrantTests {
when(authorizationRequestAuthenticationProvider.supports(eq(OAuth2AuthorizationCodeRequestAuthenticationToken.class))).thenReturn(true);
when(authorizationRequestAuthenticationProvider.authenticate(any())).thenReturn(authorizationCodeRequestAuthenticationResult);
this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.params(getAuthorizationRequestParameters(registeredClient))
.with(user("user")))
.andExpect(status().isOk());
@@ -580,8 +676,12 @@ public class OAuth2AuthorizationCodeGrantTests {
}
@Bean
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
return new JdbcRegisteredClientRepository(jdbcOperations);
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations, PasswordEncoder passwordEncoder) {
JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcOperations);
RegisteredClientParametersMapper registeredClientParametersMapper = new RegisteredClientParametersMapper();
registeredClientParametersMapper.setPasswordEncoder(passwordEncoder);
jdbcRegisteredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
return jdbcRegisteredClientRepository;
}
@Bean
@@ -594,15 +694,21 @@ public class OAuth2AuthorizationCodeGrantTests {
return jwkSource;
}
@Bean
JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getAuthorizationGrantType()) &&
OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
Authentication principal = context.getPrincipal();
Set<String> authorities = principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
Set<String> authorities = new HashSet<>();
for (GrantedAuthority authority : principal.getAuthorities()) {
authorities.add(authority.getAuthority());
}
context.getClaims().claim(AUTHORITIES_CLAIM, authorities);
}
};
@@ -677,6 +783,101 @@ public class OAuth2AuthorizationCodeGrantTests {
// @formatter:on
}
@EnableWebSecurity
static class AuthorizationServerConfigurationCustomConsentRequest extends AuthorizationServerConfiguration {
@Autowired
private RegisteredClientRepository registeredClientRepository;
@Autowired
private OAuth2AuthorizationService authorizationService;
@Autowired
private OAuth2AuthorizationConsentService authorizationConsentService;
// @formatter:off
@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint.authenticationProvider(createProvider()));
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
return http.build();
}
// @formatter:on
@Bean
@Override
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getAuthorizationGrantType()) &&
OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
OAuth2AuthorizationConsent authorizationConsent = this.authorizationConsentService.findById(
context.getRegisteredClient().getId(), context.getPrincipal().getName());
Set<String> authorities = new HashSet<>();
for (GrantedAuthority authority : authorizationConsent.getAuthorities()) {
authorities.add(authority.getAuthority());
}
context.getClaims().claim(AUTHORITIES_CLAIM, authorities);
}
};
}
private AuthenticationProvider createProvider() {
OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider =
new OAuth2AuthorizationCodeRequestAuthenticationProvider(
this.registeredClientRepository,
this.authorizationService,
this.authorizationConsentService);
authorizationCodeRequestAuthenticationProvider.setAuthorizationConsentCustomizer(new AuthorizationConsentCustomizer());
return authorizationCodeRequestAuthenticationProvider;
}
static class AuthorizationConsentCustomizer implements Consumer<OAuth2AuthorizationConsentAuthenticationContext> {
@Override
public void accept(OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext) {
OAuth2AuthorizationConsent.Builder authorizationConsentBuilder =
authorizationConsentAuthenticationContext.getAuthorizationConsent();
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
authorizationConsentAuthenticationContext.getAuthentication();
Map<String, Object> additionalParameters =
authorizationCodeRequestAuthentication.getAdditionalParameters();
RegisteredClient registeredClient = authorizationConsentAuthenticationContext.getRegisteredClient();
ClientSettings clientSettings = registeredClient.getClientSettings();
Set<String> requestedAuthorities = authorities((String) additionalParameters.get("authority"));
Set<String> allowedAuthorities = authorities(clientSettings.getSetting("custom.allowed-authorities"));
for (String requestedAuthority : requestedAuthorities) {
if (allowedAuthorities.contains(requestedAuthority)) {
authorizationConsentBuilder.authority(new SimpleGrantedAuthority(requestedAuthority));
}
}
}
private static Set<String> authorities(String param) {
Set<String> authorities = new HashSet<>();
if (param != null) {
List<String> authorityValues = Arrays.asList(param.split(" "));
authorities.addAll(authorityValues);
}
return authorities;
}
}
}
@EnableWebSecurity
static class AuthorizationServerConfigurationCustomAuthorizationEndpoint extends AuthorizationServerConfiguration {
// @formatter:off

Some files were not shown because too many files have changed in this diff Show More