234 Commits
0.1.1 ... 0.2.3

Author SHA1 Message Date
Joe Grandja
a5c5b7ea43 Release 0.2.3 2022-03-24 07:21:09 -04:00
Joe Grandja
06f1f3726d Update to Reactor 2020.0.16
Closes gh-661
2022-03-24 07:14:23 -04:00
Joe Grandja
b1d5a7a1f8 Update to Spring Security 5.5.5
Closes gh-660
2022-03-24 07:14:02 -04:00
Joe Grandja
166958364a Update to Spring Framework 5.3.16
Closes gh-659
2022-03-24 07:13:42 -04:00
Joe Grandja
16d1d871eb Update to Spring Boot 2.5.10
Closes gh-658
2022-03-24 07:13:09 -04:00
Joe Grandja
8354aaa3cc Dynamic client registration does not generate client_secret for private_key_jwt
Closes gh-657
2022-03-24 06:41:11 -04:00
Joe Grandja
586c7daf2a Apply default settings for public client type
Closes gh-656
2022-03-24 06:03:14 -04:00
Joe Grandja
12ae92b366 Polish gh-630 2022-03-24 04:50:18 -04:00
Gaurav Tiwari
7160290aaf Allow Token Introspection to be customized
Closes gh-493
2022-03-24 04:48:51 -04:00
Joe Grandja
5ab82f83cb Polish gh-628 2022-03-23 11:30:33 -04:00
Joe Grandja
a13df8a1af Move OAuth2TokenGenerator to token package
Issue gh-414
Issue gh-638
Issue gh-639
Issue gh-500
2022-03-23 05:21:32 -04:00
Steve Riesenberg
3fe6f86659 Federated Identity sample
Issue gh-538
Issue gh-499
Issue gh-106
2022-03-22 15:23:33 -04:00
Joe Grandja
ccf4a2de6e Fix expected @Transient Authentication at provider configuration endpoint
Closes gh-632
2022-03-22 15:01:06 -04:00
Joe Grandja
c2db8926df Decompose OAuth2ClientAuthenticationProvider
Closes gh-655
2022-03-22 11:30:23 -04:00
Joe Grandja
5b7d900424 Optimize InMemoryOAuth2AuthorizationService
Closes gh-654
2022-03-22 11:15:07 -04:00
Steve Riesenberg
c3402b0b12 Replaced auth-server with localhost 2022-03-17 14:59:35 -05:00
Joe Grandja
32414451f5 Add support for opaque access tokens
Closes gh-500
2022-03-03 13:30:11 -05:00
Joe Grandja
a661e1cdb7 Use OAuth2TokenGenerator for OAuth2AuthorizationCode
Closes gh-639
2022-02-25 11:28:32 -05:00
Joe Grandja
cdb48f510e Add OAuth2RefreshTokenGenerator
Closes gh-638
2022-02-25 06:03:02 -05:00
Joe Grandja
c799261a72 Introduce OAuth2TokenGenerator
Closes gh-414
2022-02-22 12:37:53 -05:00
Glen Mazza
8b32ace9e5 Add Assert.notNull() for AuthenticationProvider additions
Closes gh-530
2022-02-17 07:58:42 -05:00
Joe Grandja
3812c0e56e Next Development Version 2022-01-26 12:08:10 -05:00
Joe Grandja
cb1b23a5b2 Release 0.2.2 2022-01-26 11:53:49 -05:00
Joe Grandja
d4f539cd7a Update to Jackson 2.12.6
Closes gh-609
2022-01-26 11:46:15 -05:00
Joe Grandja
609b4a56a9 Update to Spring Boot 2.5.9
Closes gh-608
2022-01-26 11:45:47 -05:00
Joe Grandja
0b11d88a62 Update to Reactor 2020.0.15
Closes gh-607
2022-01-26 11:45:30 -05:00
Joe Grandja
37e68803b9 Update to Spring Security 5.5.4
Closes gh-606
2022-01-26 11:45:09 -05:00
Joe Grandja
22ecfd267c Update to Spring Framework 5.3.15
Closes gh-605
2022-01-26 11:44:39 -05:00
Joe Grandja
4fbe06d121 Fix inconsistent state when authorization consent is denied
Closes gh-595
2022-01-26 10:23:06 -05:00
Joe Grandja
58bac49f97 JdbcOAuth2AuthorizationService improves support for large data columns
Closes gh-604
2022-01-26 08:00:24 -05:00
Joe Grandja
f8fdcd7ae9 Polish gh-491 2022-01-25 12:55:56 -05:00
Ovidiu Popa
66bc5a0e65 Support clob and text datatype for token columns
Closes gh-480
2022-01-25 12:50:03 -05:00
Joe Grandja
e175f4fda3 Revert "Increase size for attributes column"
This reverts commit 14cb58df2b.
2022-01-25 11:00:23 -05:00
Daniel Garnier-Moiroux
a1e513b35d Throw invalid_grant when invalid token request with PKCE
Closes gh-581
2022-01-24 03:35:51 -05:00
Joe Grandja
4d5b288116 Polish token revocation
Issue gh-490
2022-01-21 14:36:16 -05:00
Joe Grandja
362c947df1 OidcProviderConfigurationHttpMessageConverter converts userinfo_endpoint
Issue gh-489
2022-01-21 09:37:15 -05:00
Joe Grandja
209248d96d Revert "Add docs outline with Antora skeleton"
This reverts commit edd7cf2434.
2022-01-21 01:47:15 -05:00
Joe Grandja
104d273ba5 Revert "Add "How-to: Implement core services with JPA""
This reverts commit 525eca63d2.
2022-01-21 01:43:02 -05:00
Joe Grandja
dce3f02f66 Revert "Use Java 8 for examples project"
This reverts commit 7b333150a2.
2022-01-21 01:41:30 -05:00
Joe Grandja
e73bef58a3 Revert "Polish "How-to: Implement core services with JPA""
This reverts commit 36d18312b0.
2022-01-21 01:39:33 -05:00
Steve Riesenberg
5412f10ff8 Polish gh-489 2022-01-20 15:40:52 -06:00
Martin Grossi
4081d460a2 Adds userinfo_endpoint to oidc provider configuration
Closes gh-488
2022-01-20 15:40:52 -06:00
Joe Grandja
30d7846122 Deprecate OAuth2TokenIntrospectionClaimAccessor
Closes gh-597
2022-01-20 06:20:18 -05:00
Joe Grandja
1476676cec Deprecate JwtEncoder and associated classes
Closes gh-596
2022-01-20 06:05:31 -05:00
Joe Grandja
165d290374 Use repo.spring.io/release for plugin repository
Issue gh-578
2022-01-18 06:49:58 -05:00
Joe Grandja
9614a252c9 Upgrade io.spring.ge.conventions to 0.0.9
Closes gh-578
2022-01-18 05:51:50 -05:00
Joe Grandja
d302444650 Introduce ProviderContext
Closes gh-479
2022-01-17 15:07:57 -05:00
Bernard Joseph Jean Bruno
1370f7e51a Update gradle enterprise 2022-01-10 13:36:44 -06:00
Steve Riesenberg
36d18312b0 Polish "How-to: Implement core services with JPA" 2022-01-10 13:34:52 -06:00
Steve Riesenberg
7b333150a2 Use Java 8 for examples project 2022-01-07 17:18:27 -06:00
Steve Riesenberg
a20f321a19 Remove docs/manual 2022-01-07 16:50:29 -06:00
Steve Riesenberg
525eca63d2 Add "How-to: Implement core services with JPA" 2022-01-07 16:50:29 -06:00
Steve Riesenberg
edd7cf2434 Add docs outline with Antora skeleton 2022-01-07 16:37:24 -06:00
Joe Grandja
f1a01597d9 Polish gh-293 2021-12-16 12:23:35 -05:00
Rafal Lewczuk
16e4f5130b Client authentication with JWT assertion
Closes gh-59
2021-12-16 06:32:25 -05:00
Steve Riesenberg
13d3567eb4 Polish gh-490 2021-12-14 17:15:59 -06:00
arfatbk
a846e936e9 Allow Token Revocation to be customized
Signed-off-by: arfatbk <arfatbk@gmail.com>
2021-12-14 12:25:32 +05:30
Joe Grandja
d0e1107f36 HttpSessionSecurityContextRepository does not persist @Transient Authentication
Related https://github.com/spring-projects/spring-security/pull/9993

Closes gh-482
2021-12-07 08:02:45 -05:00
Joe Grandja
42095a6da5 Next Development Version 2021-12-01 09:12:04 -05:00
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
Joe Grandja
2523916ca1 Release 0.1.2 2021-07-09 08:20:19 -04:00
Joe Grandja
7ffcbe57a7 Fix package tangle 2021-07-09 08:00:45 -04:00
Joe Grandja
7b4fc46369 Polish authorization consent page
Issue gh-340
2021-07-09 07:15:01 -04:00
Joe Grandja
09439a10ce Polish gh-329 2021-07-09 06:56:56 -04:00
Joe Grandja
c93c1a8097 Polish gh-338 2021-07-09 06:51:13 -04:00
Joe Grandja
1ae4f7aa13 Polish JdbcRegisteredClientRepository
Issue gh-291
2021-07-09 06:06:53 -04:00
Joe Grandja
a11284f0f5 Polish gh-331 2021-07-09 04:37:52 -04:00
Joe Grandja
ad108f519a Polish JdbcOAuth2AuthorizationConsentService
Issue gh-314
2021-07-08 09:58:07 -04:00
Joe Grandja
9787794ea1 Polish JdbcOAuth2AuthorizationService
Issue gh-304
2021-07-08 08:50:02 -04:00
Joe Grandja
41dd68903c Update spring-build-conventions to 0.0.38
Issue gh-344
2021-07-07 11:47:12 -04:00
Joe Grandja
f1834633b1 Update to use s01.oss.sonatype.org
Issue gh-344
2021-07-07 11:42:18 -04:00
Joe Grandja
1e6032e01a Update nohttp-gradle to 0.0.8
Issue gh-344
2021-07-07 11:11:45 -04:00
Joe Grandja
059880f51a Update nimbus-jose-jwt to 9.8.1
Issue gh-344
2021-07-07 10:31:37 -04:00
Joe Grandja
1d94ac85a4 Update json-path to 2.5.0
Issue gh-344
2021-07-07 10:14:03 -04:00
Joe Grandja
d6ce4c237a Update mockito-core to 3.9.0
Issue gh-344
2021-07-07 10:11:45 -04:00
Joe Grandja
4c314cf570 Update assertj-core to 3.19.0
Issue gh-344
2021-07-07 10:10:35 -04:00
Joe Grandja
d520fa7774 Update junit to 4.13.2
Issue gh-344
2021-07-07 10:08:36 -04:00
Joe Grandja
65213e97d6 Update jackson-bom to 2.12.3
Issue gh-344
2021-07-07 10:05:56 -04:00
Joe Grandja
b921b85665 Update Spring Boot to 2.5.2
Issue gh-344
2021-07-07 09:54:11 -04:00
Joe Grandja
41c38a4b53 Update Reactor to 2020.0.8
Issue gh-344
2021-07-07 09:54:11 -04:00
Joe Grandja
da945a12be Update Spring Security to 5.5.1
Issue gh-344
2021-07-07 09:54:11 -04:00
Joe Grandja
51bf550d12 Update Spring Framework to 5.3.8
Issue gh-344
2021-07-07 09:54:11 -04:00
Joe Grandja
1f17005edd Update Gradle to 6.9
Issue gh-344
2021-07-07 09:54:11 -04:00
Steve Riesenberg
4204bc7e78 Temporarily fix expires_in for access token response
TODO: This can be reverted when Spring Security 5.6 is released.

Closes gh-281
2021-07-07 08:05:44 -04:00
Anoop Garlapati
385fc37b1d Refresh token grant may issue ID token
See https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse

Closes gh-287
2021-07-07 06:18:25 -04:00
Daniel Garnier-Moiroux
b62b161b95 Include WebAuthenticationDetails in token requests
Closes gh-322
2021-07-07 04:49:26 -04:00
Joe Grandja
9def059e29 Provide configuration for the authorization endpoint
Closes gh-342
2021-07-06 18:15:59 -04:00
Joe Grandja
fb276e7a4a Provide extension for processing authorization response
Issue gh-342
2021-07-06 15:47:33 -04:00
Joe Grandja
543fa264b3 Provide extension for processing authorization request
Issue gh-342
2021-07-06 15:07:59 -04:00
Joe Grandja
cf235ceb4e Polish gh-319 2021-07-06 14:39:40 -04:00
Joe Grandja
4517022f36 Polish authorization consent
Issue gh-340 gh-280
2021-07-06 10:21:12 -04:00
Joe Grandja
08ba07d676 Extract AuthenticationProvider from OAuth2AuthorizationEndpointFilter
Closes gh-340
2021-07-06 09:16:27 -04:00
Steve Riesenberg
023e22c9d3 Update integration tests to use jdbc 2021-07-01 17:50:19 -05:00
Steve Riesenberg
549cdc7222 Update integration tests to use in-memory 2021-07-01 17:50:19 -05:00
Steve Riesenberg
4daade7366 Update authorization server sample to use jdbc
Closes gh-329
2021-07-01 14:21:41 -05:00
Steve Riesenberg
a949998664 Add test to override schema for JdbcOAuth2AuthorizationConsentService 2021-06-30 15:58:17 -05:00
Steve Riesenberg
aa208a2d30 Add correct script for testing JdbcOAuth2AuthorizationService 2021-06-30 13:05:10 -05:00
Steve Riesenberg
99fb4c8a5f Add test to override schema for JdbcOAuth2AuthorizationService 2021-06-30 12:08:35 -05:00
Steve Riesenberg
5f994a83d8 Remove public modifier from constructor 2021-06-29 16:41:16 -05:00
Steve Riesenberg
623736d640 Add test to override schema for JdbcRegisteredClientRepository 2021-06-29 15:25:07 -05:00
Steve Riesenberg
3318874da1 Polish gh-291 2021-06-29 14:06:36 -05:00
Steve Riesenberg
6f6829bb91 Polish gh-314 2021-06-29 11:03:07 -05:00
Steve Riesenberg
ab2f1749bf Polish gh-304 2021-06-29 10:50:47 -05:00
Steve Riesenberg
232b3b7ac6 Add jackson module for authorization server
Fixes problems with serialization of complex attribute values of various framework types such as OAuth2AuthorizationRequest and OAuth2ClientAuthenticationToken.

Closes gh-324
Closes gh-328
2021-06-28 16:12:18 -05:00
Steve Riesenberg
67e62a2f21 Fix NPE saving public client
Closes gh-326
2021-06-28 12:37:01 -05:00
Steve Riesenberg
473dedb9ad Revert "Polish JdbcRegisteredClientRepository, JdbcOAuth2AuthorizationConsentService, JdbcOAuth2AuthorizationService"
This reverts commit e5e391db
2021-06-25 09:14:55 -05:00
Steve Riesenberg
e5e391db38 Polish JdbcRegisteredClientRepository, JdbcOAuth2AuthorizationConsentService, JdbcOAuth2AuthorizationService 2021-06-23 16:08:23 -05:00
Joe Grandja
4209ed7599 Polish JdbcOAuth2AuthorizationService
Issue gh-304
2021-06-23 07:09:08 -04:00
Steve Riesenberg
763ef2224b Polish gh-291 2021-06-22 12:32:25 -05:00
Rafal Lewczuk
769cf8fac7 JDBC implementation of RegisteredClientRepository
Closes gh-265
2021-06-22 11:17:25 -05:00
Joe Grandja
1f4b369912 Provide configuration for the token endpoint
Closes gh-319
2021-06-22 06:13:44 -04:00
Joe Grandja
7c97d8ede9 Extract utility methods from OAuth2AuthorizationServerConfigurer
Issue gh-319
2021-06-22 06:13:34 -04:00
Joe Grandja
5a79234677 Provide extension for processing access token request
Issue gh-319
2021-06-22 06:13:25 -04:00
Joe Grandja
23732187a9 Provide extension for processing access token response
Issue gh-319
2021-06-22 06:12:44 -04:00
Steve Riesenberg
5e79f21913 Polish gh-314
Polish gh-304
2021-06-18 12:23:03 -05:00
Ovidiu Popa
5dbe973701 Provide JDBC implementation of OAuth2AuthorizationConsentService
Add new JDBC implementation of the OAuth2AuthorizationConsentService
Add equals and hashCode methods in OAuth2AuthorizationConsent

Closes gh-313
2021-06-18 11:46:45 -05:00
Steve Riesenberg
8e9563a440 Polish gh-304 2021-06-16 10:35:56 -05:00
Steve Riesenberg
566064ba9a Revert "Polish gh-313"
This reverts commit 7f095e0a
2021-06-16 10:35:13 -05:00
Steve Riesenberg
7f095e0a6f Polish gh-313 2021-06-16 10:26:56 -05:00
Ovidiu Popa
552751bd93 Provide JDBC implementation of OAuth2AuthorizationService
Add new JDBC implementation of the OAuth2AuthorizationService

Closes gh-245
2021-06-15 17:43:25 -05:00
Joe Grandja
f3cb8f758c Polish gh-280 2021-06-04 06:36:40 -04:00
Daniel Garnier-Moiroux
683dad1443 Remember user consent and make consent page configurable
Closes gh-283
2021-06-03 09:56:13 -04:00
Joe Grandja
4688b0f879 Polish gh-161 2021-06-03 09:43:10 -04:00
Steve Riesenberg
c37ecd747f OAuth2AuthorizationCodeAuthenticationProvider checks if code has expired
Closes gh-290
2021-05-26 13:41:49 -04:00
Daniel Garnier-Moiroux
6ddb73d54f Introduce integration tests for the sample oauth server
Closes gh-277
2021-05-26 13:14:53 -04:00
Joe Grandja
5e0fe9c862 Next Development Version 2021-05-07 15:17:12 -04:00
281 changed files with 26378 additions and 4708 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.

9
Jenkinsfile vendored
View File

@@ -15,10 +15,11 @@ def GRADLE_ENTERPRISE_SECRET_ACCESS_KEY = string(credentialsId: 'gradle_enterpri
variable: 'GRADLE_ENTERPRISE_ACCESS_KEY')
def SPRING_SIGNING_SECRING = file(credentialsId: 'spring-signing-secring.gpg', variable: 'SIGNING_KEYRING_FILE')
def SPRING_GPG_PASSPHRASE = string(credentialsId: 'spring-gpg-passphrase', variable: 'SIGNING_PASSWORD')
def OSSRH_CREDENTIALS = usernamePassword(credentialsId: 'oss-token', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USERNAME')
def OSSRH_S01_CREDENTIALS = usernamePassword(credentialsId: 'oss-s01-token', passwordVariable: 'OSSRH_S01_TOKEN_PASSWORD', usernameVariable: 'OSSRH_S01_TOKEN_USERNAME')
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) {
@@ -60,7 +61,7 @@ try {
sh "git clean -dfx"
withCredentials([SPRING_SIGNING_SECRING,
SPRING_GPG_PASSPHRASE,
OSSRH_CREDENTIALS,
OSSRH_S01_CREDENTIALS,
ARTIFACTORY_CREDENTIALS,
GRADLE_ENTERPRISE_CACHE_USER,
GRADLE_ENTERPRISE_SECRET_ACCESS_KEY]) {
@@ -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' -PossrhUsername=$OSSRH_USERNAME -PossrhPassword=$OSSRH_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

@@ -1,8 +1,8 @@
buildscript {
dependencies {
classpath 'io.spring.gradle:spring-build-conventions:0.0.37'
classpath 'io.spring.gradle:spring-build-conventions:0.0.38'
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
classpath 'io.spring.nohttp:nohttp-gradle:0.0.5.RELEASE'
classpath 'io.spring.nohttp:nohttp-gradle:0.0.8'
}
repositories {
maven {
@@ -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

@@ -1,37 +0,0 @@
apply plugin: 'io.spring.convention.docs'
apply plugin: 'io.spring.convention.springdependencymangement'
apply plugin: 'io.spring.convention.dependency-set'
apply plugin: 'io.spring.convention.repository'
apply plugin: 'java'
asciidoctor {
attributes([stylesheet: 'css/style.css'])
resources {
from(sourceDir) {
include "css/**"
}
}
}
asciidoctorj {
def ghTag = snapshotBuild ? 'main' : project.version
def ghUrl = "https://github.com/spring-projects-experimental/spring-authorization-server/tree/$ghTag"
attributes 'spring-authorization-server-version' : project.version,
'spring-boot-version' : springBootVersion,
revnumber : project.version,
'gh-url': ghUrl,
'gh-samples-url': "$ghUrl/samples"
attributeProvider resolvedVersions(project.configurations.testCompile)
}
def resolvedVersions(Configuration configuration) {
return {
configuration.resolvedConfiguration
.resolvedArtifacts
.collectEntries { [(it.name + "-version"): it.moduleVersion.id.version] }
}
}
repositories {
maven { url "https://repo.spring.io/release" }
}

View File

@@ -1,5 +0,0 @@
@import 'spring.css';
a code {
color: #097dff;
}

View File

@@ -1,15 +0,0 @@
= Spring Authorization Server Reference
Joe Grandja
:include-dir: _includes
:security-api-url: https://docs.spring.io/spring-authorization-server/site/docs/current/api/
:source-indent: 0
:tabsize: 4
:toc: left
== Preface
#TODO:# Document preface
== Introduction
#TODO:# Document introduction

View File

@@ -1,5 +1,5 @@
version=0.1.1
springBootVersion=2.4.5
version=0.2.3
springBootVersion=2.5.10
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.6"
ext.springVersion = "5.3.16"
}
if (!project.hasProperty("springSecurityVersion")) {
ext.springSecurityVersion = "5.4.6"
ext.springSecurityVersion = "5.5.5"
}
if (!project.hasProperty("reactorVersion")) {
ext.reactorVersion = "2020.0.6"
ext.reactorVersion = "2020.0.16"
}
if (!project.hasProperty("locksDisabled")) {
@@ -21,16 +21,18 @@ 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.0"
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.6"
}
dependencies {
dependency "com.nimbusds:nimbus-jose-jwt:9.10.1"
dependency "javax.servlet:javax.servlet-api:4.0.1"
dependency 'junit:junit:4.13.1'
dependency 'org.assertj:assertj-core:3.18.1'
dependency 'org.mockito:mockito-core:3.6.28'
dependency 'junit:junit:4.13.2'
dependency 'org.assertj:assertj-core:3.19.0'
dependency 'org.mockito:mockito-core:3.9.0'
dependency "com.squareup.okhttp3:mockwebserver:3.14.9"
dependency "com.squareup.okhttp3:okhttp:3.14.9"
dependency "com.jayway.jsonpath:json-path:2.4.0"
dependency "com.jayway.jsonpath:json-path:2.5.0"
dependency "org.hsqldb:hsqldb:2.5.2"
}
}

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -10,12 +10,18 @@ dependencies {
compile 'com.nimbusds:nimbus-jose-jwt'
compile 'com.fasterxml.jackson.core:jackson-databind'
optional 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
optional 'org.springframework:spring-jdbc'
testCompile 'org.springframework.security:spring-security-test'
testCompile 'org.springframework:spring-webmvc'
testCompile 'junit:junit'
testCompile 'org.assertj:assertj-core'
testCompile 'org.mockito:mockito-core'
testCompile 'com.jayway.jsonpath:json-path'
testCompile 'com.squareup.okhttp3:mockwebserver'
testRuntime 'org.hsqldb:hsqldb'
provided 'javax.servlet:javax.servlet-api'
}

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

@@ -0,0 +1,49 @@
/*
* 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.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Base configurer for an OAuth 2.0 component (e.g. protocol endpoint).
*
* @author Joe Grandja
* @since 0.1.2
*/
abstract class AbstractOAuth2Configurer {
private final ObjectPostProcessor<Object> objectPostProcessor;
AbstractOAuth2Configurer(ObjectPostProcessor<Object> objectPostProcessor) {
this.objectPostProcessor = objectPostProcessor;
}
abstract <B extends HttpSecurityBuilder<B>> void init(B builder);
abstract <B extends HttpSecurityBuilder<B>> void configure(B builder);
abstract RequestMatcher getRequestMatcher();
protected final <T> T postProcess(T object) {
return (T) this.objectPostProcessor.postProcess(object);
}
protected final ObjectPostProcessor<Object> getObjectPostProcessor() {
return this.objectPostProcessor;
}
}

View File

@@ -0,0 +1,211 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.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.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
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;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Configurer for the OAuth 2.0 Authorization Endpoint.
*
* @author Joe Grandja
* @since 0.1.2
* @see OAuth2AuthorizationServerConfigurer#authorizationEndpoint
* @see OAuth2AuthorizationEndpointFilter
*/
public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private AuthenticationConverter authorizationRequestConverter;
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private AuthenticationSuccessHandler authorizationResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
private String consentPage;
/**
* Restrict for internal use only.
*/
OAuth2AuthorizationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
* to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request.
*
* @param authorizationRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2AuthorizationEndpointConfigurer authorizationRequestConverter(AuthenticationConverter authorizationRequestConverter) {
this.authorizationRequestConverter = authorizationRequestConverter;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
*
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2AuthorizationEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
* and returning the {@link OAuth2AuthorizationResponse Authorization Response}.
*
* @param authorizationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2AuthorizationEndpointConfigurer authorizationResponseHandler(AuthenticationSuccessHandler authorizationResponseHandler) {
this.authorizationResponseHandler = authorizationResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
* and returning the {@link OAuth2Error Error Response}.
*
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2AuthorizationEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
/**
* Specify the URI to redirect Resource Owners to if consent is required during
* the {@code authorization_code} flow. A default consent page will be generated when
* this attribute is not specified.
*
* If a URI is specified, applications are required to process the specified URI to generate
* a consent page. The query string will contain the following parameters:
*
* <ul>
* <li>{@code client_id} - the client identifier</li>
* <li>{@code scope} - a space-delimited list of scopes present in the authorization request</li>
* <li>{@code state} - a CSRF protection token</li>
* </ul>
*
* In general, the consent page should create a form that submits
* a request with the following requirements:
*
* <ul>
* <li>It must be an HTTP POST</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}
* consented to as an HTTP parameter</li>
* </ul>
*
* @param consentPage the URI of the custom consent page to redirect to if consent is required (e.g. "/oauth2/consent")
* @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
*/
public OAuth2AuthorizationEndpointConfigurer consentPage(String consentPage) {
this.consentPage = consentPage;
return this;
}
@Override
<B extends HttpSecurityBuilder<B>> void init(B builder) {
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
this.requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(
providerSettings.getAuthorizationEndpoint(),
HttpMethod.GET.name()),
new AntPathRequestMatcher(
providerSettings.getAuthorizationEndpoint(),
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);
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
new OAuth2AuthorizationEndpointFilter(
authenticationManager,
providerSettings.getAuthorizationEndpoint());
if (this.authorizationRequestConverter != null) {
authorizationEndpointFilter.setAuthenticationConverter(this.authorizationRequestConverter);
}
if (this.authorizationResponseHandler != null) {
authorizationEndpointFilter.setAuthenticationSuccessHandler(this.authorizationResponseHandler);
}
if (this.errorResponseHandler != null) {
authorizationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
if (StringUtils.hasText(this.consentPage)) {
authorizationEndpointFilter.setConsentPage(this.consentPage);
}
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider =
new OAuth2AuthorizationCodeRequestAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
OAuth2ConfigurerUtils.getAuthorizationService(builder),
OAuth2ConfigurerUtils.getAuthorizationConsentService(builder));
authenticationProviders.add(authorizationCodeRequestAuthenticationProvider);
return authenticationProviders;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,55 +16,50 @@
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.net.URI;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import javax.servlet.AsyncContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import com.nimbusds.jose.jwk.source.JWKSource;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
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.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.Transient;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.oauth2.core.OAuth2Token;
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.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
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.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.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
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.OAuth2TokenEndpointFilter;
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;
import org.springframework.security.oauth2.server.authorization.web.ProviderContextFilter;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.context.SecurityContextRepository;
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;
/**
* An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support.
@@ -73,40 +68,35 @@ import org.springframework.util.StringUtils;
* @author Daniel Garnier-Moiroux
* @author Gerardo Roza
* @author Ovidiu Popa
* @author Gaurav Tiwari
* @since 0.0.1
* @see AbstractHttpConfigurer
* @see OAuth2ClientAuthenticationConfigurer
* @see OAuth2AuthorizationEndpointConfigurer
* @see OAuth2TokenEndpointConfigurer
* @see OAuth2TokenIntrospectionEndpointConfigurer
* @see OAuth2TokenRevocationEndpointConfigurer
* @see OidcConfigurer
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @see OAuth2AuthorizationEndpointFilter
* @see OAuth2TokenEndpointFilter
* @see OAuth2TokenIntrospectionEndpointFilter
* @see OAuth2TokenRevocationEndpointFilter
* @see OAuth2AuthorizationConsentService
* @see NimbusJwkSetEndpointFilter
* @see OidcProviderConfigurationEndpointFilter
* @see OAuth2AuthorizationServerMetadataEndpointFilter
* @see OAuth2ClientAuthenticationFilter
* @see OidcClientRegistrationEndpointFilter
*/
public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBuilder<B>>
extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer<B>, B> {
private RequestMatcher authorizationEndpointMatcher;
private RequestMatcher tokenEndpointMatcher;
private RequestMatcher tokenIntrospectionEndpointMatcher;
private RequestMatcher tokenRevocationEndpointMatcher;
private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = createConfigurers();
private RequestMatcher jwkSetEndpointMatcher;
private RequestMatcher oidcProviderConfigurationEndpointMatcher;
private RequestMatcher authorizationServerMetadataEndpointMatcher;
private RequestMatcher oidcClientRegistrationEndpointMatcher;
private final RequestMatcher endpointsMatcher = (request) ->
this.authorizationEndpointMatcher.matches(request) ||
this.tokenEndpointMatcher.matches(request) ||
this.tokenIntrospectionEndpointMatcher.matches(request) ||
this.tokenRevocationEndpointMatcher.matches(request) ||
getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) ||
getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) ||
getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class).matches(request) ||
getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class).matches(request) ||
getRequestMatcher(OidcConfigurer.class).matches(request) ||
this.jwkSetEndpointMatcher.matches(request) ||
this.oidcProviderConfigurationEndpointMatcher.matches(request) ||
this.authorizationServerMetadataEndpointMatcher.matches(request) ||
this.oidcClientRegistrationEndpointMatcher.matches(request);
this.authorizationServerMetadataEndpointMatcher.matches(request);
/**
* Sets the repository of registered clients.
@@ -116,7 +106,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;
}
@@ -128,7 +118,19 @@ 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;
}
/**
* Sets the authorization consent service.
*
* @param authorizationConsentService the authorization consent service
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer<B> authorizationConsentService(OAuth2AuthorizationConsentService authorizationConsentService) {
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
getBuilder().setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
return this;
}
@@ -140,7 +142,88 @@ 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;
}
/**
* Sets the token generator.
*
* @param tokenGenerator the token generator
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
* @since 0.2.3
*/
public OAuth2AuthorizationServerConfigurer<B> tokenGenerator(OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
getBuilder().setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
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;
}
/**
* Configures the OAuth 2.0 Authorization Endpoint.
*
* @param authorizationEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2AuthorizationEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer<B> authorizationEndpoint(Customizer<OAuth2AuthorizationEndpointConfigurer> authorizationEndpointCustomizer) {
authorizationEndpointCustomizer.customize(getConfigurer(OAuth2AuthorizationEndpointConfigurer.class));
return this;
}
/**
* Configures the OAuth 2.0 Token Endpoint.
*
* @param tokenEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2TokenEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer<B> tokenEndpoint(Customizer<OAuth2TokenEndpointConfigurer> tokenEndpointCustomizer) {
tokenEndpointCustomizer.customize(getConfigurer(OAuth2TokenEndpointConfigurer.class));
return this;
}
/**
* Configures the OAuth 2.0 Token Introspection Endpoint.
*
* @param tokenIntrospectionEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2TokenIntrospectionEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
* @since 0.2.3
*/
public OAuth2AuthorizationServerConfigurer<B> tokenIntrospectionEndpoint(Customizer<OAuth2TokenIntrospectionEndpointConfigurer> tokenIntrospectionEndpointCustomizer) {
tokenIntrospectionEndpointCustomizer.customize(getConfigurer(OAuth2TokenIntrospectionEndpointConfigurer.class));
return this;
}
/**
* Configures the OAuth 2.0 Token Revocation Endpoint.
*
* @param tokenRevocationEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2TokenRevocationEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
* @since 0.2.2
*/
public OAuth2AuthorizationServerConfigurer<B> tokenRevocationEndpoint(Customizer<OAuth2TokenRevocationEndpointConfigurer> tokenRevocationEndpointCustomizer) {
tokenRevocationEndpointCustomizer.customize(getConfigurer(OAuth2TokenRevocationEndpointConfigurer.class));
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;
}
@@ -155,282 +238,201 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
@Override
public void init(B builder) {
ProviderSettings providerSettings = getProviderSettings(builder);
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
validateProviderSettings(providerSettings);
initEndpointMatchers(providerSettings);
OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
new OAuth2ClientAuthenticationProvider(
getRegisteredClientRepository(builder),
getAuthorizationService(builder));
PasswordEncoder passwordEncoder = getOptionalBean(builder, PasswordEncoder.class);
if (passwordEncoder != null) {
clientAuthenticationProvider.setPasswordEncoder(passwordEncoder);
}
builder.authenticationProvider(postProcess(clientAuthenticationProvider));
JwtEncoder jwtEncoder = getJwtEncoder(builder);
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = getJwtCustomizer(builder);
OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider =
new OAuth2AuthorizationCodeAuthenticationProvider(
getAuthorizationService(builder),
jwtEncoder);
if (jwtCustomizer != null) {
authorizationCodeAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
}
builder.authenticationProvider(postProcess(authorizationCodeAuthenticationProvider));
OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =
new OAuth2RefreshTokenAuthenticationProvider(
getAuthorizationService(builder),
jwtEncoder);
if (jwtCustomizer != null) {
refreshTokenAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
}
builder.authenticationProvider(postProcess(refreshTokenAuthenticationProvider));
OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider =
new OAuth2ClientCredentialsAuthenticationProvider(
getAuthorizationService(builder),
jwtEncoder);
if (jwtCustomizer != null) {
clientCredentialsAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
}
builder.authenticationProvider(postProcess(clientCredentialsAuthenticationProvider));
OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =
new OAuth2TokenIntrospectionAuthenticationProvider(
getRegisteredClientRepository(builder),
getAuthorizationService(builder));
builder.authenticationProvider(postProcess(tokenIntrospectionAuthenticationProvider));
OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =
new OAuth2TokenRevocationAuthenticationProvider(
getAuthorizationService(builder));
builder.authenticationProvider(postProcess(tokenRevocationAuthenticationProvider));
// TODO Make OpenID Client Registration an "opt-in" feature
OidcClientRegistrationAuthenticationProvider oidcClientRegistrationAuthenticationProvider =
new OidcClientRegistrationAuthenticationProvider(
getRegisteredClientRepository(builder),
getAuthorizationService(builder));
builder.authenticationProvider(postProcess(oidcClientRegistrationAuthenticationProvider));
this.configurers.values().forEach(configurer -> configurer.init(builder));
ExceptionHandlingConfigurer<B> exceptionHandling = builder.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionHandling != null) {
exceptionHandling.defaultAuthenticationEntryPointFor(
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
new OrRequestMatcher(
this.tokenEndpointMatcher,
this.tokenIntrospectionEndpointMatcher,
this.tokenRevocationEndpointMatcher)
getRequestMatcher(OAuth2TokenEndpointConfigurer.class),
getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class),
getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class))
);
}
// gh-482
initSecurityContextRepository(builder);
}
private void initSecurityContextRepository(B builder) {
// TODO This is a temporary fix and should be removed after upgrading to Spring Security 5.7.0 GA.
//
// See:
// Prevent Save @Transient Authentication with existing HttpSession
// https://github.com/spring-projects/spring-security/pull/9993
final SecurityContextRepository securityContextRepository = builder.getSharedObject(SecurityContextRepository.class);
if (!(securityContextRepository instanceof HttpSessionSecurityContextRepository)) {
return;
}
SecurityContextRepository securityContextRepositoryTransientNotSaved = new SecurityContextRepository() {
private final RequestMatcher clientAuthenticationRequestMatcher = initClientAuthenticationRequestMatcher();
private final RequestMatcher jwtAuthenticationRequestMatcher = initJwtAuthenticationRequestMatcher();
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
final HttpServletRequest unwrappedRequest = requestResponseHolder.getRequest();
final HttpServletResponse unwrappedResponse = requestResponseHolder.getResponse();
SecurityContext securityContext = securityContextRepository.loadContext(requestResponseHolder);
if (this.clientAuthenticationRequestMatcher.matches(unwrappedRequest) ||
this.jwtAuthenticationRequestMatcher.matches(unwrappedRequest)) {
final SaveContextOnUpdateOrErrorResponseWrapper transientAuthenticationResponseWrapper =
new SaveContextOnUpdateOrErrorResponseWrapper(unwrappedResponse, false) {
@Override
protected void saveContext(SecurityContext context) {
// @Transient Authentication should not be saved
if (context.getAuthentication() != null) {
Assert.state(isTransientAuthentication(context.getAuthentication()), "Expected @Transient Authentication");
}
}
};
// Override the default HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper
requestResponseHolder.setResponse(transientAuthenticationResponseWrapper);
final HttpServletRequestWrapper transientAuthenticationRequestWrapper =
new HttpServletRequestWrapper(unwrappedRequest) {
@Override
public AsyncContext startAsync() {
transientAuthenticationResponseWrapper.disableSaveOnResponseCommitted();
return super.startAsync();
}
@Override
public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse)
throws IllegalStateException {
transientAuthenticationResponseWrapper.disableSaveOnResponseCommitted();
return super.startAsync(servletRequest, servletResponse);
}
};
// Override the default HttpSessionSecurityContextRepository.SaveToSessionRequestWrapper
requestResponseHolder.setRequest(transientAuthenticationRequestWrapper);
}
return securityContext;
}
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
Authentication authentication = context.getAuthentication();
if (authentication == null || isTransientAuthentication(authentication)) {
return;
}
securityContextRepository.saveContext(context, request, response);
}
@Override
public boolean containsContext(HttpServletRequest request) {
return securityContextRepository.containsContext(request);
}
private boolean isTransientAuthentication(Authentication authentication) {
return AnnotationUtils.getAnnotation(authentication.getClass(), Transient.class) != null;
}
private RequestMatcher initClientAuthenticationRequestMatcher() {
// OAuth2ClientAuthenticationToken is @Transient and is accepted by
// OAuth2TokenEndpointFilter, OAuth2TokenIntrospectionEndpointFilter and OAuth2TokenRevocationEndpointFilter
List<RequestMatcher> requestMatchers = new ArrayList<>();
requestMatchers.add(getRequestMatcher(OAuth2TokenEndpointConfigurer.class));
requestMatchers.add(getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class));
requestMatchers.add(getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class));
return new OrRequestMatcher(requestMatchers);
}
private RequestMatcher initJwtAuthenticationRequestMatcher() {
// JwtAuthenticationToken is @Transient and is accepted by
// OidcUserInfoEndpointFilter and OidcClientRegistrationEndpointFilter
List<RequestMatcher> requestMatchers = new ArrayList<>();
requestMatchers.add(
getConfigurer(OidcConfigurer.class)
.getConfigurer(OidcUserInfoEndpointConfigurer.class).getRequestMatcher()
);
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
getConfigurer(OidcConfigurer.class)
.getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
if (clientRegistrationEndpointConfigurer != null) {
requestMatchers.add(clientRegistrationEndpointConfigurer.getRequestMatcher());
}
return new OrRequestMatcher(requestMatchers);
}
};
builder.setSharedObject(SecurityContextRepository.class, securityContextRepositoryTransientNotSaved);
}
@Override
public void configure(B builder) {
ProviderSettings providerSettings = getProviderSettings(builder);
if (providerSettings.issuer() != null) {
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
new OidcProviderConfigurationEndpointFilter(providerSettings);
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
this.configurers.values().forEach(configurer -> configurer.configure(builder));
OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings);
builder.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
ProviderContextFilter providerContextFilter = new ProviderContextFilter(providerSettings);
builder.addFilterAfter(postProcess(providerContextFilter), SecurityContextPersistenceFilter.class);
JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(builder);
if (jwkSource != null) {
NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
jwkSource, providerSettings.getJwkSetEndpoint());
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
jwkSource,
providerSettings.jwkSetEndpoint());
builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings);
builder.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.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(OAuth2TokenIntrospectionEndpointConfigurer.class, new OAuth2TokenIntrospectionEndpointConfigurer(this::postProcess));
configurers.put(OAuth2TokenRevocationEndpointConfigurer.class, new OAuth2TokenRevocationEndpointConfigurer(this::postProcess));
configurers.put(OidcConfigurer.class, new OidcConfigurer(this::postProcess));
return configurers;
}
OAuth2ClientAuthenticationFilter clientAuthenticationFilter =
new OAuth2ClientAuthenticationFilter(
authenticationManager,
new OrRequestMatcher(
this.tokenEndpointMatcher,
this.tokenIntrospectionEndpointMatcher,
this.tokenRevocationEndpointMatcher));
builder.addFilterAfter(postProcess(clientAuthenticationFilter), AbstractPreAuthenticatedProcessingFilter.class);
@SuppressWarnings("unchecked")
private <T> T getConfigurer(Class<T> type) {
return (T) this.configurers.get(type);
}
OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
new OAuth2AuthorizationEndpointFilter(
getRegisteredClientRepository(builder),
getAuthorizationService(builder),
providerSettings.authorizationEndpoint());
builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
OAuth2TokenEndpointFilter tokenEndpointFilter =
new OAuth2TokenEndpointFilter(
authenticationManager,
providerSettings.tokenEndpoint());
builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
OAuth2TokenIntrospectionEndpointFilter tokenIntrospectionEndpointFilter =
new OAuth2TokenIntrospectionEndpointFilter(
authenticationManager,
providerSettings.tokenIntrospectionEndpoint());
builder.addFilterAfter(postProcess(tokenIntrospectionEndpointFilter), OAuth2TokenEndpointFilter.class);
OAuth2TokenRevocationEndpointFilter tokenRevocationEndpointFilter =
new OAuth2TokenRevocationEndpointFilter(
authenticationManager,
providerSettings.tokenRevocationEndpoint());
builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), OAuth2TokenIntrospectionEndpointFilter.class);
// TODO Make OpenID Client Registration an "opt-in" feature
OidcClientRegistrationEndpointFilter oidcClientRegistrationEndpointFilter =
new OidcClientRegistrationEndpointFilter(
authenticationManager,
providerSettings.oidcClientRegistrationEndpoint());
builder.addFilterAfter(postProcess(oidcClientRegistrationEndpointFilter), OAuth2TokenRevocationEndpointFilter.class);
private <T extends AbstractOAuth2Configurer> RequestMatcher getRequestMatcher(Class<T> configurerType) {
return getConfigurer(configurerType).getRequestMatcher();
}
private void initEndpointMatchers(ProviderSettings providerSettings) {
this.authorizationEndpointMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(
providerSettings.authorizationEndpoint(),
HttpMethod.GET.name()),
new AntPathRequestMatcher(
providerSettings.authorizationEndpoint(),
HttpMethod.POST.name()));
this.tokenEndpointMatcher = new AntPathRequestMatcher(
providerSettings.tokenEndpoint(), HttpMethod.POST.name());
this.tokenIntrospectionEndpointMatcher = new AntPathRequestMatcher(
providerSettings.tokenIntrospectionEndpoint(), HttpMethod.POST.name());
this.tokenRevocationEndpointMatcher = new AntPathRequestMatcher(
providerSettings.tokenRevocationEndpoint(), 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);
}
}
}
private static <B extends HttpSecurityBuilder<B>> RegisteredClientRepository getRegisteredClientRepository(B builder) {
RegisteredClientRepository registeredClientRepository = builder.getSharedObject(RegisteredClientRepository.class);
if (registeredClientRepository == null) {
registeredClientRepository = getBean(builder, RegisteredClientRepository.class);
builder.setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
}
return registeredClientRepository;
}
private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService getAuthorizationService(B builder) {
OAuth2AuthorizationService authorizationService = builder.getSharedObject(OAuth2AuthorizationService.class);
if (authorizationService == null) {
authorizationService = getOptionalBean(builder, OAuth2AuthorizationService.class);
if (authorizationService == null) {
authorizationService = new InMemoryOAuth2AuthorizationService();
}
builder.setSharedObject(OAuth2AuthorizationService.class, authorizationService);
}
return authorizationService;
}
private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
if (jwtEncoder == null) {
jwtEncoder = getOptionalBean(builder, JwtEncoder.class);
if (jwtEncoder == null) {
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
jwtEncoder = new NimbusJwsEncoder(jwkSource);
}
builder.setSharedObject(JwtEncoder.class, jwtEncoder);
}
return jwtEncoder;
}
@SuppressWarnings("unchecked")
private static <B extends HttpSecurityBuilder<B>> JWKSource<SecurityContext> getJwkSource(B builder) {
JWKSource<SecurityContext> jwkSource = builder.getSharedObject(JWKSource.class);
if (jwkSource == null) {
ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class);
jwkSource = getBean(builder, type);
builder.setSharedObject(JWKSource.class, jwkSource);
}
return jwkSource;
}
@SuppressWarnings("unchecked")
private static <B extends HttpSecurityBuilder<B>> OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(B builder) {
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = builder.getSharedObject(OAuth2TokenCustomizer.class);
if (jwtCustomizer == null) {
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, JwtEncodingContext.class);
jwtCustomizer = getOptionalBean(builder, type);
if (jwtCustomizer != null) {
builder.setSharedObject(OAuth2TokenCustomizer.class, jwtCustomizer);
}
}
return jwtCustomizer;
}
private static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettings(B builder) {
ProviderSettings providerSettings = builder.getSharedObject(ProviderSettings.class);
if (providerSettings == null) {
providerSettings = getOptionalBean(builder, ProviderSettings.class);
if (providerSettings == null) {
providerSettings = new ProviderSettings();
}
builder.setSharedObject(ProviderSettings.class, providerSettings);
}
return providerSettings;
}
private static <B extends HttpSecurityBuilder<B>, T> T getBean(B builder, Class<T> type) {
return builder.getSharedObject(ApplicationContext.class).getBean(type);
}
@SuppressWarnings("unchecked")
private static <B extends HttpSecurityBuilder<B>, T> T getBean(B builder, ResolvableType type) {
ApplicationContext context = builder.getSharedObject(ApplicationContext.class);
String[] names = context.getBeanNamesForType(type);
if (names.length == 1) {
return (T) context.getBean(names[0]);
}
if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
}
throw new NoSuchBeanDefinitionException(type);
}
private static <B extends HttpSecurityBuilder<B>, T> T getOptionalBean(B builder, Class<T> type) {
Map<String, T> beansMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(
builder.getSharedObject(ApplicationContext.class), type);
if (beansMap.size() > 1) {
throw new NoUniqueBeanDefinitionException(type, beansMap.size(),
"Expected single matching bean of type '" + type.getName() + "' but found " +
beansMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(beansMap.keySet()));
}
return (!beansMap.isEmpty() ? beansMap.values().iterator().next() : null);
}
@SuppressWarnings("unchecked")
private static <B extends HttpSecurityBuilder<B>, T> T getOptionalBean(B builder, ResolvableType type) {
ApplicationContext context = builder.getSharedObject(ApplicationContext.class);
String[] names = context.getBeanNamesForType(type);
if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
}
return names.length == 1 ? (T) context.getBean(names[0]) : null;
}
}

View File

@@ -0,0 +1,187 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.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.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.JwtClientAssertionAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.PublicClientAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.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;
import org.springframework.util.Assert;
/**
* 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) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
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<>();
RegisteredClientRepository registeredClientRepository = OAuth2ConfigurerUtils.getRegisteredClientRepository(builder);
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(builder);
JwtClientAssertionAuthenticationProvider jwtClientAssertionAuthenticationProvider =
new JwtClientAssertionAuthenticationProvider(registeredClientRepository, authorizationService);
authenticationProviders.add(jwtClientAssertionAuthenticationProvider);
ClientSecretAuthenticationProvider clientSecretAuthenticationProvider =
new ClientSecretAuthenticationProvider(registeredClientRepository, authorizationService);
PasswordEncoder passwordEncoder = OAuth2ConfigurerUtils.getOptionalBean(builder, PasswordEncoder.class);
if (passwordEncoder != null) {
clientSecretAuthenticationProvider.setPasswordEncoder(passwordEncoder);
}
authenticationProviders.add(clientSecretAuthenticationProvider);
PublicClientAuthenticationProvider publicClientAuthenticationProvider =
new PublicClientAuthenticationProvider(registeredClientRepository, authorizationService);
authenticationProviders.add(publicClientAuthenticationProvider);
return authenticationProviders;
}
}

View File

@@ -0,0 +1,221 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.Map;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.util.StringUtils;
/**
* Utility methods for the OAuth 2.0 Configurers.
*
* @author Joe Grandja
* @since 0.1.2
*/
final class OAuth2ConfigurerUtils {
private OAuth2ConfigurerUtils() {
}
static <B extends HttpSecurityBuilder<B>> RegisteredClientRepository getRegisteredClientRepository(B builder) {
RegisteredClientRepository registeredClientRepository = builder.getSharedObject(RegisteredClientRepository.class);
if (registeredClientRepository == null) {
registeredClientRepository = getBean(builder, RegisteredClientRepository.class);
builder.setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
}
return registeredClientRepository;
}
static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationService getAuthorizationService(B builder) {
OAuth2AuthorizationService authorizationService = builder.getSharedObject(OAuth2AuthorizationService.class);
if (authorizationService == null) {
authorizationService = getOptionalBean(builder, OAuth2AuthorizationService.class);
if (authorizationService == null) {
authorizationService = new InMemoryOAuth2AuthorizationService();
}
builder.setSharedObject(OAuth2AuthorizationService.class, authorizationService);
}
return authorizationService;
}
static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizationConsentService getAuthorizationConsentService(B builder) {
OAuth2AuthorizationConsentService authorizationConsentService = builder.getSharedObject(OAuth2AuthorizationConsentService.class);
if (authorizationConsentService == null) {
authorizationConsentService = getOptionalBean(builder, OAuth2AuthorizationConsentService.class);
if (authorizationConsentService == null) {
authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService();
}
builder.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
}
return authorizationConsentService;
}
@SuppressWarnings("unchecked")
static <B extends HttpSecurityBuilder<B>> OAuth2TokenGenerator<? extends OAuth2Token> getTokenGenerator(B builder) {
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = builder.getSharedObject(OAuth2TokenGenerator.class);
if (tokenGenerator == null) {
tokenGenerator = getOptionalBean(builder, OAuth2TokenGenerator.class);
if (tokenGenerator == null) {
JwtGenerator jwtGenerator = getJwtGenerator(builder);
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer = getAccessTokenCustomizer(builder);
if (accessTokenCustomizer != null) {
accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer);
}
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
if (jwtGenerator != null) {
tokenGenerator = new DelegatingOAuth2TokenGenerator(
jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
} else {
tokenGenerator = new DelegatingOAuth2TokenGenerator(
accessTokenGenerator, refreshTokenGenerator);
}
}
builder.setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
}
return tokenGenerator;
}
private static <B extends HttpSecurityBuilder<B>> JwtGenerator getJwtGenerator(B builder) {
JwtGenerator jwtGenerator = builder.getSharedObject(JwtGenerator.class);
if (jwtGenerator == null) {
JwtEncoder jwtEncoder = getJwtEncoder(builder);
if (jwtEncoder != null) {
jwtGenerator = new JwtGenerator(jwtEncoder);
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = getJwtCustomizer(builder);
if (jwtCustomizer != null) {
jwtGenerator.setJwtCustomizer(jwtCustomizer);
}
builder.setSharedObject(JwtGenerator.class, jwtGenerator);
}
}
return jwtGenerator;
}
private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
if (jwtEncoder == null) {
jwtEncoder = getOptionalBean(builder, JwtEncoder.class);
if (jwtEncoder == null) {
JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
if (jwkSource != null) {
jwtEncoder = new NimbusJwsEncoder(jwkSource);
}
}
if (jwtEncoder != null) {
builder.setSharedObject(JwtEncoder.class, jwtEncoder);
}
}
return jwtEncoder;
}
@SuppressWarnings("unchecked")
static <B extends HttpSecurityBuilder<B>> JWKSource<SecurityContext> getJwkSource(B builder) {
JWKSource<SecurityContext> jwkSource = builder.getSharedObject(JWKSource.class);
if (jwkSource == null) {
ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class);
jwkSource = getOptionalBean(builder, type);
if (jwkSource != null) {
builder.setSharedObject(JWKSource.class, jwkSource);
}
}
return jwkSource;
}
private static <B extends HttpSecurityBuilder<B>> OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(B builder) {
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, JwtEncodingContext.class);
return getOptionalBean(builder, type);
}
private static <B extends HttpSecurityBuilder<B>> OAuth2TokenCustomizer<OAuth2TokenClaimsContext> getAccessTokenCustomizer(B builder) {
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, OAuth2TokenClaimsContext.class);
return getOptionalBean(builder, type);
}
static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettings(B builder) {
ProviderSettings providerSettings = builder.getSharedObject(ProviderSettings.class);
if (providerSettings == null) {
providerSettings = getBean(builder, ProviderSettings.class);
builder.setSharedObject(ProviderSettings.class, providerSettings);
}
return providerSettings;
}
static <B extends HttpSecurityBuilder<B>, T> T getBean(B builder, Class<T> type) {
return builder.getSharedObject(ApplicationContext.class).getBean(type);
}
@SuppressWarnings("unchecked")
static <B extends HttpSecurityBuilder<B>, T> T getBean(B builder, ResolvableType type) {
ApplicationContext context = builder.getSharedObject(ApplicationContext.class);
String[] names = context.getBeanNamesForType(type);
if (names.length == 1) {
return (T) context.getBean(names[0]);
}
if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
}
throw new NoSuchBeanDefinitionException(type);
}
static <B extends HttpSecurityBuilder<B>, T> T getOptionalBean(B builder, Class<T> type) {
Map<String, T> beansMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(
builder.getSharedObject(ApplicationContext.class), type);
if (beansMap.size() > 1) {
throw new NoUniqueBeanDefinitionException(type, beansMap.size(),
"Expected single matching bean of type '" + type.getName() + "' but found " +
beansMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(beansMap.keySet()));
}
return (!beansMap.isEmpty() ? beansMap.values().iterator().next() : null);
}
@SuppressWarnings("unchecked")
static <B extends HttpSecurityBuilder<B>, T> T getOptionalBean(B builder, ResolvableType type) {
ApplicationContext context = builder.getSharedObject(ApplicationContext.class);
String[] names = context.getBeanNamesForType(type);
if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
}
return names.length == 1 ? (T) context.getBean(names[0]) : null;
}
}

View File

@@ -0,0 +1,181 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.LinkedList;
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.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
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.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for the OAuth 2.0 Token Endpoint.
*
* @author Joe Grandja
* @since 0.1.2
* @see OAuth2AuthorizationServerConfigurer#tokenEndpoint
* @see OAuth2TokenEndpointFilter
*/
public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private AuthenticationConverter accessTokenRequestConverter;
private final List<AuthenticationProvider> authenticationProviders = new LinkedList<>();
private AuthenticationSuccessHandler accessTokenResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
/**
* Restrict for internal use only.
*/
OAuth2TokenEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* 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 accessTokenRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest}
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
*/
public OAuth2TokenEndpointConfigurer accessTokenRequestConverter(AuthenticationConverter accessTokenRequestConverter) {
this.accessTokenRequestConverter = accessTokenRequestConverter;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2AuthorizationGrantAuthenticationToken}.
*
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2AuthorizationGrantAuthenticationToken}
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
*/
public OAuth2TokenEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AccessTokenAuthenticationToken}
* and returning the {@link OAuth2AccessTokenResponse Access Token Response}.
*
* @param accessTokenResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AccessTokenAuthenticationToken}
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
*/
public OAuth2TokenEndpointConfigurer accessTokenResponseHandler(AuthenticationSuccessHandler accessTokenResponseHandler) {
this.accessTokenResponseHandler = accessTokenResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
* and returning the {@link OAuth2Error Error Response}.
*
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
* @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
*/
public OAuth2TokenEndpointConfigurer 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 AntPathRequestMatcher(
providerSettings.getTokenEndpoint(), 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);
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
OAuth2TokenEndpointFilter tokenEndpointFilter =
new OAuth2TokenEndpointFilter(
authenticationManager,
providerSettings.getTokenEndpoint());
if (this.accessTokenRequestConverter != null) {
tokenEndpointFilter.setAuthenticationConverter(this.accessTokenRequestConverter);
}
if (this.accessTokenResponseHandler != null) {
tokenEndpointFilter.setAuthenticationSuccessHandler(this.accessTokenResponseHandler);
}
if (this.errorResponseHandler != null) {
tokenEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(builder);
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = OAuth2ConfigurerUtils.getTokenGenerator(builder);
OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider =
new OAuth2AuthorizationCodeAuthenticationProvider(authorizationService, tokenGenerator);
authenticationProviders.add(authorizationCodeAuthenticationProvider);
OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =
new OAuth2RefreshTokenAuthenticationProvider(authorizationService, tokenGenerator);
authenticationProviders.add(refreshTokenAuthenticationProvider);
OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider =
new OAuth2ClientCredentialsAuthenticationProvider(authorizationService, tokenGenerator);
authenticationProviders.add(clientCredentialsAuthenticationProvider);
return authenticationProviders;
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.LinkedList;
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.Customizer;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for the OAuth 2.0 Token Introspection Endpoint.
*
* @author Gaurav Tiwari
* @since 0.2.3
* @see OAuth2AuthorizationServerConfigurer#tokenIntrospectionEndpoint(Customizer)
* @see OAuth2TokenIntrospectionEndpointFilter
*/
public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private AuthenticationConverter introspectionRequestConverter;
private final List<AuthenticationProvider> authenticationProviders = new LinkedList<>();
private AuthenticationSuccessHandler introspectionResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
/**
* Restrict for internal use only.
*/
OAuth2TokenIntrospectionEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Sets the {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest}
* to an instance of {@link OAuth2TokenIntrospectionAuthenticationToken} used for authenticating the request.
*
* @param introspectionRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Introspection Request from {@link HttpServletRequest}
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration
*/
public OAuth2TokenIntrospectionEndpointConfigurer introspectionRequestConverter(AuthenticationConverter introspectionRequestConverter) {
this.introspectionRequestConverter = introspectionRequestConverter;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenIntrospectionAuthenticationToken}.
*
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenIntrospectionAuthenticationToken}
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration
*/
public OAuth2TokenIntrospectionEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}.
*
* @param introspectionResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration
*/
public OAuth2TokenIntrospectionEndpointConfigurer introspectionResponseHandler(AuthenticationSuccessHandler introspectionResponseHandler) {
this.introspectionResponseHandler = introspectionResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
* and returning the {@link OAuth2Error Error Response}.
*
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
* @return the {@link OAuth2TokenIntrospectionEndpointConfigurer} for further configuration
*/
public OAuth2TokenIntrospectionEndpointConfigurer 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 AntPathRequestMatcher(
providerSettings.getTokenIntrospectionEndpoint(), 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);
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
OAuth2TokenIntrospectionEndpointFilter introspectionEndpointFilter =
new OAuth2TokenIntrospectionEndpointFilter(
authenticationManager, providerSettings.getTokenIntrospectionEndpoint());
if (this.introspectionRequestConverter != null) {
introspectionEndpointFilter.setAuthenticationConverter(this.introspectionRequestConverter);
}
if (this.introspectionResponseHandler != null) {
introspectionEndpointFilter.setAuthenticationSuccessHandler(this.introspectionResponseHandler);
}
if (this.errorResponseHandler != null) {
introspectionEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
builder.addFilterAfter(postProcess(introspectionEndpointFilter), FilterSecurityInterceptor.class);
}
@Override
public RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =
new OAuth2TokenIntrospectionAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
OAuth2ConfigurerUtils.getAuthorizationService(builder));
authenticationProviders.add(tokenIntrospectionAuthenticationProvider);
return authenticationProviders;
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.LinkedList;
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.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for the OAuth 2.0 Token Revocation Endpoint.
*
* @author Arfat Chaus
* @since 0.2.2
* @see OAuth2AuthorizationServerConfigurer#tokenRevocationEndpoint
* @see OAuth2TokenRevocationEndpointFilter
*/
public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private AuthenticationConverter revocationRequestConverter;
private final List<AuthenticationProvider> authenticationProviders = new LinkedList<>();
private AuthenticationSuccessHandler revocationResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
/**
* Restrict for internal use only.
*/
OAuth2TokenRevocationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Sets the {@link AuthenticationConverter} used when attempting to extract a Revoke Token Request from {@link HttpServletRequest}
* to an instance of {@link OAuth2TokenRevocationAuthenticationToken} used for authenticating the client.
*
* @param revocationRequestConverter the {@link AuthenticationConverter} used when attempting to extract client credentials from {@link HttpServletRequest}
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration
*/
public OAuth2TokenRevocationEndpointConfigurer revocationRequestConverter(AuthenticationConverter revocationRequestConverter) {
this.revocationRequestConverter = revocationRequestConverter;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenRevocationAuthenticationToken}.
*
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenRevocationAuthenticationToken}
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration
*/
public OAuth2TokenRevocationEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenRevocationAuthenticationToken}.
*
* @param revocationResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenRevocationAuthenticationToken}
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration
*/
public OAuth2TokenRevocationEndpointConfigurer revocationResponseHandler(AuthenticationSuccessHandler revocationResponseHandler) {
this.revocationResponseHandler = revocationResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
* and returning the {@link OAuth2Error Error Response}.
*
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
* @return the {@link OAuth2TokenRevocationEndpointConfigurer} for further configuration
*/
public OAuth2TokenRevocationEndpointConfigurer 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 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);
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
OAuth2TokenRevocationEndpointFilter revocationEndpointFilter =
new OAuth2TokenRevocationEndpointFilter(
authenticationManager, providerSettings.getTokenRevocationEndpoint());
if (this.revocationRequestConverter != null) {
revocationEndpointFilter.setAuthenticationConverter(this.revocationRequestConverter);
}
if (this.revocationResponseHandler != null) {
revocationEndpointFilter.setAuthenticationSuccessHandler(this.revocationResponseHandler);
}
if (this.errorResponseHandler != null) {
revocationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
builder.addFilterAfter(postProcess(revocationEndpointFilter), FilterSecurityInterceptor.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =
new OAuth2TokenRevocationAuthenticationProvider(OAuth2ConfigurerUtils.getAuthorizationService(builder));
authenticationProviders.add(tokenRevocationAuthenticationProvider);
return authenticationProviders;
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.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.getTokenGenerator(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,137 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.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 Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
private RequestMatcher requestMatcher;
/**
* Restrict for internal use only.
*/
OidcConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
addConfigurer(OidcUserInfoEndpointConfigurer.class, 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) {
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
if (clientRegistrationEndpointConfigurer == null) {
addConfigurer(OidcClientRegistrationEndpointConfigurer.class,
new OidcClientRegistrationEndpointConfigurer(getObjectPostProcessor()));
clientRegistrationEndpointConfigurer = getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
}
clientRegistrationEndpointCustomizer.customize(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(getConfigurer(OidcUserInfoEndpointConfigurer.class));
return this;
}
@Override
<B extends HttpSecurityBuilder<B>> void init(B builder) {
OidcUserInfoEndpointConfigurer userInfoEndpointConfigurer =
getConfigurer(OidcUserInfoEndpointConfigurer.class);
userInfoEndpointConfigurer.init(builder);
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
if (clientRegistrationEndpointConfigurer != null) {
clientRegistrationEndpointConfigurer.init(builder);
}
List<RequestMatcher> requestMatchers = new ArrayList<>();
requestMatchers.add(new AntPathRequestMatcher(
"/.well-known/openid-configuration", HttpMethod.GET.name()));
requestMatchers.add(userInfoEndpointConfigurer.getRequestMatcher());
if (clientRegistrationEndpointConfigurer != null) {
requestMatchers.add(clientRegistrationEndpointConfigurer.getRequestMatcher());
}
this.requestMatcher = new OrRequestMatcher(requestMatchers);
}
@Override
<B extends HttpSecurityBuilder<B>> void configure(B builder) {
OidcUserInfoEndpointConfigurer userInfoEndpointConfigurer =
getConfigurer(OidcUserInfoEndpointConfigurer.class);
userInfoEndpointConfigurer.configure(builder);
OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
if (clientRegistrationEndpointConfigurer != null) {
clientRegistrationEndpointConfigurer.configure(builder);
}
ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
new OidcProviderConfigurationEndpointFilter(providerSettings);
builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
@SuppressWarnings("unchecked")
<T> T getConfigurer(Class<T> type) {
return (T) this.configurers.get(type);
}
private <T extends AbstractOAuth2Configurer> void addConfigurer(Class<T> configurerType, T configurer) {
this.configurers.put(configurerType, configurer);
}
}

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

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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.
@@ -391,7 +391,7 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth
valuesConsumer.accept(values);
}
private static void validateURL(Object url, String errorMessage) {
protected static void validateURL(Object url, String errorMessage) {
if (URL.class.isAssignableFrom(url.getClass())) {
return;
}

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,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

@@ -0,0 +1,81 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.core;
import java.io.Serializable;
import org.springframework.util.Assert;
/**
* Standard data formats for OAuth 2.0 Tokens.
*
* @author Joe Grandja
* @since 0.2.3
*/
public final class OAuth2TokenFormat implements Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
/**
* Self-contained tokens use a protected, time-limited data structure that contains token metadata
* and claims of the user and/or client. JSON Web Token (JWT) is a widely used format.
*/
public static final OAuth2TokenFormat SELF_CONTAINED = new OAuth2TokenFormat("self-contained");
/**
* Reference (opaque) tokens are unique identifiers that serve as a reference
* to the token metadata and claims of the user and/or client, stored at the provider.
*/
public static final OAuth2TokenFormat REFERENCE = new OAuth2TokenFormat("reference");
private final String value;
/**
* Constructs an {@code OAuth2TokenFormat} using the provided value.
*
* @param value the value of the token format
*/
public OAuth2TokenFormat(String value) {
Assert.hasText(value, "value cannot be empty");
this.value = value;
}
/**
* Returns the value of the token format.
*
* @return the value of the token format
*/
public String getValue() {
return this.value;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
OAuth2TokenFormat that = (OAuth2TokenFormat) obj;
return getValue().equals(that.getValue());
}
@Override
public int hashCode() {
return getValue().hashCode();
}
}

View File

@@ -41,7 +41,9 @@ import java.util.List;
* @see OAuth2TokenIntrospectionClaimNames
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc7662#section-2.2">Introspection Response</a>
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/597">gh-597</a>
*/
@Deprecated
public interface OAuth2TokenIntrospectionClaimAccessor extends ClaimAccessor {
/**
@@ -56,7 +58,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

@@ -34,7 +34,9 @@ package org.springframework.security.oauth2.core;
*
* @author Josh Cummings
* @since 5.2
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/597">gh-597</a>
*/
@Deprecated
public interface OAuth2TokenIntrospectionClaimNames {
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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 = 1;
private static final int MINOR = 2;
private static final int PATCH = 3;
/**
* 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

@@ -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,11 +15,15 @@
*/
package org.springframework.security.oauth2.core.oidc;
import java.net.URL;
import java.time.Instant;
import java.util.List;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
/**
* A {@link ClaimAccessor} for the "claims" that are contained
@@ -98,6 +102,18 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
return getClaimAsString(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
}
/**
* Returns the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate
* the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
* {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods {@code (token_endpoint_auth_signing_alg)}.
*
* @return the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate the Client at the Token Endpoint
* @since 0.2.2
*/
default String getTokenEndpointAuthenticationSigningAlgorithm() {
return getClaimAsString(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG);
}
/**
* Returns the OAuth 2.0 {@code grant_type} values that the Client will restrict itself to using {@code (grant_types)}.
*
@@ -125,6 +141,16 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
return getClaimAsStringList(OidcClientMetadataClaimNames.SCOPE);
}
/**
* Returns the {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}.
*
* @return the {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}
* @since 0.2.2
*/
default URL getJwkSetUrl() {
return getClaimAsURL(OidcClientMetadataClaimNames.JWKS_URI);
}
/**
* Returns the {@link SignatureAlgorithm JWS} algorithm required for signing the {@link OidcIdToken ID Token} issued to the Client {@code (id_token_signed_response_alg)}.
*
@@ -134,4 +160,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

@@ -15,7 +15,9 @@
*/
package org.springframework.security.oauth2.core.oidc;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
/**
* The names of the "claims" defined by OpenID Connect Dynamic Client Registration 1.0
@@ -63,6 +65,14 @@ public interface OidcClientMetadataClaimNames {
*/
String TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method";
/**
* {@code token_endpoint_auth_signing_alg} - the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT}
* used to authenticate the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
* {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods
* @since 0.2.2
*/
String TOKEN_ENDPOINT_AUTH_SIGNING_ALG = "token_endpoint_auth_signing_alg";
/**
* {@code grant_types} - the OAuth 2.0 {@code grant_type} values that the Client will restrict itself to using
*/
@@ -78,9 +88,27 @@ public interface OidcClientMetadataClaimNames {
*/
String SCOPE = "scope";
/**
* {@code jwks_uri} - the {@code URL} for the Client's JSON Web Key Set
* @since 0.2.2
*/
String JWKS_URI = "jwks_uri";
/**
* {@code id_token_signed_response_alg} - the {@link JwsAlgorithm JWS} algorithm required for signing the {@link OidcIdToken ID Token} issued to the Client
*/
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

@@ -26,8 +26,11 @@ import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.Version;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
/**
@@ -174,6 +177,20 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
return claim(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, tokenEndpointAuthenticationMethod);
}
/**
* Sets the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate
* the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
* {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods, OPTIONAL.
* @param authenticationSigningAlgorithm the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT}
* used to authenticate the Client at the Token Endpoint
* @return the {@link Builder} for further configuration
* @since 0.2.2
*/
public Builder tokenEndpointAuthenticationSigningAlgorithm(String authenticationSigningAlgorithm) {
return claim(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, authenticationSigningAlgorithm);
}
/**
* Add the OAuth 2.0 {@code grant_type} that the Client will restrict itself to using, OPTIONAL.
*
@@ -243,6 +260,17 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
return this;
}
/**
* Sets the {@code URL} for the Client's JSON Web Key Set, OPTIONAL.
*
* @param jwkSetUrl the {@code URL} for the Client's JSON Web Key Set
* @return the {@link Builder} for further configuration
* @since 0.2.2
*/
public Builder jwkSetUrl(String jwkSetUrl) {
return claim(OidcClientMetadataClaimNames.JWKS_URI, jwkSetUrl);
}
/**
* Sets the {@link SignatureAlgorithm JWS} algorithm required for signing the {@link OidcIdToken ID Token} issued to the Client, OPTIONAL.
*
@@ -253,6 +281,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 +357,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");
@@ -322,6 +369,9 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.SCOPE), "scope must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.SCOPE), "scope cannot be empty");
}
if (this.claims.get(OidcClientMetadataClaimNames.JWKS_URI) != null) {
validateURL(this.claims.get(OidcClientMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL");
}
}
@SuppressWarnings("unchecked")
@@ -345,11 +395,14 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
if (URL.class.isAssignableFrom(url.getClass())) {
return;
}
try {
new URI(url.toString()).toURL();
} catch (Exception ex) {
throw new IllegalArgumentException(errorMessage, ex);
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -118,6 +118,17 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization
return this;
}
/**
* Use this {@code userinfo_endpoint} in the resulting {@link OidcProviderConfiguration}, OPTIONAL.
*
* @param userInfoEndpoint the {@code URL} of the OpenID Connect 1.0 UserInfo Endpoint
* @return the {@link Builder} for further configuration
* @since 0.2.2
*/
public Builder userInfoEndpoint(String userInfoEndpoint) {
return claim(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, userInfoEndpoint);
}
/**
* Validate the claims and build the {@link OidcProviderConfiguration}.
* <p>
@@ -144,6 +155,9 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization
Assert.notNull(getClaims().get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be null");
Assert.isInstanceOf(List.class, getClaims().get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms must be of type List");
Assert.notEmpty((List<?>) getClaims().get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be empty");
if (getClaims().get(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT) != null) {
validateURL(getClaims().get(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT), "userInfoEndpoint must be a valid URL");
}
}
@SuppressWarnings("unchecked")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package org.springframework.security.oauth2.core.oidc;
import java.net.URL;
import java.util.List;
import org.springframework.security.oauth2.core.ClaimAccessor;
@@ -56,4 +57,14 @@ public interface OidcProviderMetadataClaimAccessor extends OAuth2AuthorizationSe
return getClaimAsStringList(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED);
}
/**
* Returns the {@code URL} of the OpenID Connect 1.0 UserInfo Endpoint {@code (userinfo_endpoint)}.
*
* @return the {@code URL} of the OpenID Connect 1.0 UserInfo Endpoint
* @since 0.2.2
*/
default URL getUserInfoEndpoint() {
return getClaimAsURL(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -39,4 +39,10 @@ public interface OidcProviderMetadataClaimNames extends OAuth2AuthorizationServe
*/
String ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = "id_token_signing_alg_values_supported";
/**
* {@code userinfo_endpoint} - the {@code URL} of the OpenID Connect 1.0 UserInfo Endpoint
* @since 0.2.2
*/
String USER_INFO_ENDPOINT = "userinfo_endpoint";
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.security.oauth2.core.oidc.http.converter;
import java.net.URL;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
@@ -130,6 +131,7 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess
private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.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 URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
private static final Converter<Object, ?> INSTANT_CONVERTER = getConverter(INSTANT_TYPE_DESCRIPTOR);
private final ClaimTypeConverter claimTypeConverter;
@@ -137,6 +139,7 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess
Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
Converter<Object, ?> collectionStringConverter = getConverter(
TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
claimConverters.put(OidcClientMetadataClaimNames.CLIENT_ID, stringConverter);
@@ -146,9 +149,11 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess
claimConverters.put(OidcClientMetadataClaimNames.CLIENT_NAME, stringConverter);
claimConverters.put(OidcClientMetadataClaimNames.REDIRECT_URIS, collectionStringConverter);
claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, stringConverter);
claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, stringConverter);
claimConverters.put(OidcClientMetadataClaimNames.GRANT_TYPES, collectionStringConverter);
claimConverters.put(OidcClientMetadataClaimNames.RESPONSE_TYPES, collectionStringConverter);
claimConverters.put(OidcClientMetadataClaimNames.SCOPE, MapOidcClientRegistrationConverter::convertScope);
claimConverters.put(OidcClientMetadataClaimNames.JWKS_URI, urlConverter);
claimConverters.put(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, stringConverter);
this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -140,6 +140,7 @@ public class OidcProviderConfigurationHttpMessageConverter
claimConverters.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT, urlConverter);
claimConverters.put(OidcProviderMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
claimConverters.put(OidcProviderMetadataClaimNames.JWKS_URI, urlConverter);
claimConverters.put(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, urlConverter);
claimConverters.put(OidcProviderMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, collectionStringConverter);
claimConverters.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, collectionStringConverter);
claimConverters.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, collectionStringConverter);

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

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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,10 +36,12 @@ 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>
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/596">gh-596</a>
*/
@Deprecated
public final class JoseHeader {
private final Map<String, Object> headers;
@@ -48,12 +50,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 +65,7 @@ public final class JoseHeader {
*
* @return the JWK Set URL
*/
public URL getJwkSetUri() {
public URL getJwkSetUrl() {
return getHeader(JoseHeaderNames.JKU);
}
@@ -91,13 +94,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 +131,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 +149,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 +191,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 +219,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 +229,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 +275,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 +318,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 +338,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 +382,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

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,7 +27,9 @@ package org.springframework.security.oauth2.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>
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/596">gh-596</a>
*/
@Deprecated
public final class JoseHeaderNames {
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,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,8 +34,10 @@ 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>
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/596">gh-596</a>
*/
@Deprecated
public final class JwtClaimsSet implements JwtClaimAccessor {
private final Map<String, Object> claims;
@@ -166,10 +170,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 +187,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

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,12 +32,14 @@ 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>
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/596">gh-596</a>
*/
@Deprecated
@FunctionalInterface
public interface JwtEncoder {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,9 @@ package org.springframework.security.oauth2.jwt;
*
* @author Joe Grandja
* @since 0.0.1
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/596">gh-596</a>
*/
@Deprecated
public class JwtEncodingException extends JwtException {
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,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;
@@ -43,7 +45,6 @@ import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import net.minidev.json.JSONObject;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -63,27 +64,19 @@ 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>
* @deprecated See <a target="_blank" href="https://github.com/spring-projects/spring-authorization-server/issues/596">gh-596</a>
*/
@Deprecated
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;
/**
@@ -101,108 +94,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(new JSONObject(jwk)));
}
catch (Exception ex) {
builder.jwk(JWK.parse(jwk));
} 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);
}
}
@@ -211,14 +222,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();
@@ -231,27 +245,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> {
@@ -260,9 +290,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();
@@ -275,11 +306,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));
@@ -290,15 +316,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

@@ -0,0 +1,106 @@
/*
* 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;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An {@link OAuth2AuthorizationConsentService} that stores {@link OAuth2AuthorizationConsent}'s in-memory.
*
* <p>
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
*
* @author Daniel Garnier-Moiroux
* @since 0.1.2
* @see OAuth2AuthorizationConsentService
*/
public final class InMemoryOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
private final Map<Integer, OAuth2AuthorizationConsent> authorizationConsents = new ConcurrentHashMap<>();
/**
* Constructs an {@code InMemoryOAuth2AuthorizationConsentService}.
*/
public InMemoryOAuth2AuthorizationConsentService() {
this(Collections.emptyList());
}
/**
* Constructs an {@code InMemoryOAuth2AuthorizationConsentService} using the provided parameters.
*
* @param authorizationConsents the authorization consent(s)
*/
public InMemoryOAuth2AuthorizationConsentService(OAuth2AuthorizationConsent... authorizationConsents) {
this(Arrays.asList(authorizationConsents));
}
/**
* Constructs an {@code InMemoryOAuth2AuthorizationConsentService} using the provided parameters.
*
* @param authorizationConsents the authorization consent(s)
*/
public InMemoryOAuth2AuthorizationConsentService(List<OAuth2AuthorizationConsent> authorizationConsents) {
Assert.notNull(authorizationConsents, "authorizationConsents cannot be null");
authorizationConsents.forEach(authorizationConsent -> {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
int id = getId(authorizationConsent);
Assert.isTrue(!this.authorizationConsents.containsKey(id),
"The authorizationConsent must be unique. Found duplicate, with registered client id: ["
+ authorizationConsent.getRegisteredClientId()
+ "] and principal name: [" + authorizationConsent.getPrincipalName() + "]");
this.authorizationConsents.put(id, authorizationConsent);
});
}
@Override
public void save(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
int id = getId(authorizationConsent);
this.authorizationConsents.put(id, authorizationConsent);
}
@Override
public void remove(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
int id = getId(authorizationConsent);
this.authorizationConsents.remove(id, authorizationConsent);
}
@Override
@Nullable
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
int id = getId(registeredClientId, principalName);
return this.authorizationConsents.get(id);
}
private static int getId(String registeredClientId, String principalName) {
return Objects.hash(registeredClientId, principalName);
}
private static int getId(OAuth2AuthorizationConsent authorizationConsent) {
return getId(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,12 +17,14 @@ package org.springframework.security.oauth2.server.authorization;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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;
@@ -40,8 +42,29 @@ import org.springframework.util.Assert;
* @see OAuth2AuthorizationService
*/
public final class InMemoryOAuth2AuthorizationService implements OAuth2AuthorizationService {
private int maxInitializedAuthorizations = 100;
/*
* Stores "initialized" (uncompleted) authorizations, where an access token has not yet been granted.
* This state occurs with the authorization_code grant flow during the user consent step OR
* when the code is returned in the authorization response but the access token request is not yet initiated.
*/
private Map<String, OAuth2Authorization> initializedAuthorizations =
Collections.synchronizedMap(new MaxSizeHashMap<>(this.maxInitializedAuthorizations));
/*
* Stores "completed" authorizations, where an access token has been granted.
*/
private final Map<String, OAuth2Authorization> authorizations = new ConcurrentHashMap<>();
/*
* Constructor used for testing only.
*/
InMemoryOAuth2AuthorizationService(int maxInitializedAuthorizations) {
this.maxInitializedAuthorizations = maxInitializedAuthorizations;
this.initializedAuthorizations = Collections.synchronizedMap(new MaxSizeHashMap<>(this.maxInitializedAuthorizations));
}
/**
* Constructs an {@code InMemoryOAuth2AuthorizationService}.
*/
@@ -76,30 +99,52 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
@Override
public void save(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
this.authorizations.put(authorization.getId(), authorization);
if (isComplete(authorization)) {
this.authorizations.put(authorization.getId(), authorization);
} else {
this.initializedAuthorizations.put(authorization.getId(), authorization);
}
}
@Override
public void remove(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
this.authorizations.remove(authorization.getId(), authorization);
if (isComplete(authorization)) {
this.authorizations.remove(authorization.getId(), authorization);
} else {
this.initializedAuthorizations.remove(authorization.getId(), authorization);
}
}
@Nullable
@Override
public OAuth2Authorization findById(String id) {
Assert.hasText(id, "id cannot be empty");
return this.authorizations.get(id);
OAuth2Authorization authorization = this.authorizations.get(id);
return authorization != null ?
authorization :
this.initializedAuthorizations.get(id);
}
@Nullable
@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;
}
}
for (OAuth2Authorization authorization : this.initializedAuthorizations.values()) {
if (hasToken(authorization, token, tokenType)) {
return authorization;
}
}
return null;
}
private static boolean isComplete(OAuth2Authorization authorization) {
return authorization.getAccessToken() != null;
}
private static boolean hasToken(OAuth2Authorization authorization, String token, @Nullable OAuth2TokenType tokenType) {
@@ -141,4 +186,19 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
authorization.getToken(OAuth2RefreshToken.class);
return refreshToken != null && refreshToken.getToken().getTokenValue().equals(token);
}
private static final class MaxSizeHashMap<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
private MaxSizeHashMap(int maxSize) {
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > this.maxSize;
}
}
}

View File

@@ -0,0 +1,261 @@
/*
* 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;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
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.lang.Nullable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.StringUtils;
/**
* A JDBC implementation of an {@link OAuth2AuthorizationConsentService} that uses a
* {@link JdbcOperations} for {@link OAuth2AuthorizationConsent} persistence.
*
* <p>
* <b>NOTE:</b> This {@code OAuth2AuthorizationConsentService} depends on the table definition
* described in
* "classpath:org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql" and
* therefore MUST be defined in the database schema.
*
* @author Ovidiu Popa
* @since 0.1.2
* @see OAuth2AuthorizationConsentService
* @see OAuth2AuthorizationConsent
* @see JdbcOperations
* @see RowMapper
*/
public class JdbcOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
// @formatter:off
private static final String COLUMN_NAMES = "registered_client_id, "
+ "principal_name, "
+ "authorities";
// @formatter:on
private static final String TABLE_NAME = "oauth2_authorization_consent";
private static final String PK_FILTER = "registered_client_id = ? AND principal_name = ?";
// @formatter:off
private static final String LOAD_AUTHORIZATION_CONSENT_SQL = "SELECT " + COLUMN_NAMES
+ " FROM " + TABLE_NAME
+ " WHERE " + PK_FILTER;
// @formatter:on
// @formatter:off
private static final String SAVE_AUTHORIZATION_CONSENT_SQL = "INSERT INTO " + TABLE_NAME
+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?)";
// @formatter:on
// @formatter:off
private static final String UPDATE_AUTHORIZATION_CONSENT_SQL = "UPDATE " + TABLE_NAME
+ " SET authorities = ?"
+ " WHERE " + PK_FILTER;
// @formatter:on
private static final String REMOVE_AUTHORIZATION_CONSENT_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + PK_FILTER;
private final JdbcOperations jdbcOperations;
private RowMapper<OAuth2AuthorizationConsent> authorizationConsentRowMapper;
private Function<OAuth2AuthorizationConsent, List<SqlParameterValue>> authorizationConsentParametersMapper;
/**
* Constructs a {@code JdbcOAuth2AuthorizationConsentService} using the provided parameters.
*
* @param jdbcOperations the JDBC operations
* @param registeredClientRepository the registered client repository
*/
public JdbcOAuth2AuthorizationConsentService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
this.jdbcOperations = jdbcOperations;
this.authorizationConsentRowMapper = new OAuth2AuthorizationConsentRowMapper(registeredClientRepository);
this.authorizationConsentParametersMapper = new OAuth2AuthorizationConsentParametersMapper();
}
@Override
public void save(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
OAuth2AuthorizationConsent existingAuthorizationConsent = findById(
authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
if (existingAuthorizationConsent == null) {
insertAuthorizationConsent(authorizationConsent);
} else {
updateAuthorizationConsent(authorizationConsent);
}
}
private void updateAuthorizationConsent(OAuth2AuthorizationConsent authorizationConsent) {
List<SqlParameterValue> parameters = this.authorizationConsentParametersMapper.apply(authorizationConsent);
SqlParameterValue registeredClientId = parameters.remove(0);
SqlParameterValue principalName = parameters.remove(0);
parameters.add(registeredClientId);
parameters.add(principalName);
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
this.jdbcOperations.update(UPDATE_AUTHORIZATION_CONSENT_SQL, pss);
}
private void insertAuthorizationConsent(OAuth2AuthorizationConsent authorizationConsent) {
List<SqlParameterValue> parameters = this.authorizationConsentParametersMapper.apply(authorizationConsent);
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
this.jdbcOperations.update(SAVE_AUTHORIZATION_CONSENT_SQL, pss);
}
@Override
public void remove(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
SqlParameterValue[] parameters = new SqlParameterValue[] {
new SqlParameterValue(Types.VARCHAR, authorizationConsent.getRegisteredClientId()),
new SqlParameterValue(Types.VARCHAR, authorizationConsent.getPrincipalName())
};
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters);
this.jdbcOperations.update(REMOVE_AUTHORIZATION_CONSENT_SQL, pss);
}
@Override
@Nullable
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
SqlParameterValue[] parameters = new SqlParameterValue[] {
new SqlParameterValue(Types.VARCHAR, registeredClientId),
new SqlParameterValue(Types.VARCHAR, principalName)};
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters);
List<OAuth2AuthorizationConsent> result = this.jdbcOperations.query(LOAD_AUTHORIZATION_CONSENT_SQL, pss,
this.authorizationConsentRowMapper);
return !result.isEmpty() ? result.get(0) : null;
}
/**
* Sets the {@link RowMapper} used for mapping the current row in
* {@code java.sql.ResultSet} to {@link OAuth2AuthorizationConsent}. The default is
* {@link OAuth2AuthorizationConsentRowMapper}.
*
* @param authorizationConsentRowMapper the {@link RowMapper} used for mapping the current
* row in {@code ResultSet} to {@link OAuth2AuthorizationConsent}
*/
public final void setAuthorizationConsentRowMapper(RowMapper<OAuth2AuthorizationConsent> authorizationConsentRowMapper) {
Assert.notNull(authorizationConsentRowMapper, "authorizationConsentRowMapper cannot be null");
this.authorizationConsentRowMapper = authorizationConsentRowMapper;
}
/**
* Sets the {@code Function} used for mapping {@link OAuth2AuthorizationConsent} to
* a {@code List} of {@link SqlParameterValue}. The default is
* {@link OAuth2AuthorizationConsentParametersMapper}.
*
* @param authorizationConsentParametersMapper the {@code Function} used for mapping
* {@link OAuth2AuthorizationConsent} to a {@code List} of {@link SqlParameterValue}
*/
public final void setAuthorizationConsentParametersMapper(
Function<OAuth2AuthorizationConsent, List<SqlParameterValue>> authorizationConsentParametersMapper) {
Assert.notNull(authorizationConsentParametersMapper, "authorizationConsentParametersMapper cannot be null");
this.authorizationConsentParametersMapper = authorizationConsentParametersMapper;
}
protected final JdbcOperations getJdbcOperations() {
return this.jdbcOperations;
}
protected final RowMapper<OAuth2AuthorizationConsent> getAuthorizationConsentRowMapper() {
return this.authorizationConsentRowMapper;
}
protected final Function<OAuth2AuthorizationConsent, List<SqlParameterValue>> getAuthorizationConsentParametersMapper() {
return this.authorizationConsentParametersMapper;
}
/**
* The default {@link RowMapper} that maps the current row in
* {@code ResultSet} to {@link OAuth2AuthorizationConsent}.
*/
public static class OAuth2AuthorizationConsentRowMapper implements RowMapper<OAuth2AuthorizationConsent> {
private final RegisteredClientRepository registeredClientRepository;
public OAuth2AuthorizationConsentRowMapper(RegisteredClientRepository registeredClientRepository) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
this.registeredClientRepository = registeredClientRepository;
}
@Override
public OAuth2AuthorizationConsent mapRow(ResultSet rs, int rowNum) throws SQLException {
String registeredClientId = rs.getString("registered_client_id");
RegisteredClient registeredClient = this.registeredClientRepository.findById(registeredClientId);
if (registeredClient == null) {
throw new DataRetrievalFailureException(
"The RegisteredClient with id '" + registeredClientId + "' was not found in the RegisteredClientRepository.");
}
String principalName = rs.getString("principal_name");
OAuth2AuthorizationConsent.Builder builder = OAuth2AuthorizationConsent.withId(registeredClientId, principalName);
String authorizationConsentAuthorities = rs.getString("authorities");
if (authorizationConsentAuthorities != null) {
for (String authority : StringUtils.commaDelimitedListToSet(authorizationConsentAuthorities)) {
builder.authority(new SimpleGrantedAuthority(authority));
}
}
return builder.build();
}
protected final RegisteredClientRepository getRegisteredClientRepository() {
return this.registeredClientRepository;
}
}
/**
* The default {@code Function} that maps {@link OAuth2AuthorizationConsent} to a
* {@code List} of {@link SqlParameterValue}.
*/
public static class OAuth2AuthorizationConsentParametersMapper implements Function<OAuth2AuthorizationConsent, List<SqlParameterValue>> {
@Override
public List<SqlParameterValue> apply(OAuth2AuthorizationConsent authorizationConsent) {
List<SqlParameterValue> parameters = new ArrayList<>();
parameters.add(new SqlParameterValue(Types.VARCHAR, authorizationConsent.getRegisteredClientId()));
parameters.add(new SqlParameterValue(Types.VARCHAR, authorizationConsent.getPrincipalName()));
Set<String> authorities = new HashSet<>();
for (GrantedAuthority authority : authorizationConsent.getAuthorities()) {
authorities.add(authority.getAuthority());
}
parameters.add(new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToDelimitedString(authorities, ",")));
return parameters;
}
}
}

View File

@@ -0,0 +1,686 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization;
import java.nio.charset.StandardCharsets;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
import org.springframework.jdbc.core.ConnectionCallback;
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.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobCreator;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.lang.Nullable;
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.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
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.jackson2.OAuth2AuthorizationServerJackson2Module;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* A JDBC implementation of an {@link OAuth2AuthorizationService} that uses a
* {@link JdbcOperations} for {@link OAuth2Authorization} persistence.
*
* <p>
* <b>NOTE:</b> This {@code OAuth2AuthorizationService} depends on the table definition
* described in
* "classpath:org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql" and
* therefore MUST be defined in the database schema.
*
* @author Ovidiu Popa
* @author Joe Grandja
* @since 0.1.2
* @see OAuth2AuthorizationService
* @see OAuth2Authorization
* @see JdbcOperations
* @see RowMapper
*/
public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationService {
// @formatter:off
private static final String COLUMN_NAMES = "id, "
+ "registered_client_id, "
+ "principal_name, "
+ "authorization_grant_type, "
+ "attributes, "
+ "state, "
+ "authorization_code_value, "
+ "authorization_code_issued_at, "
+ "authorization_code_expires_at,"
+ "authorization_code_metadata,"
+ "access_token_value,"
+ "access_token_issued_at,"
+ "access_token_expires_at,"
+ "access_token_metadata,"
+ "access_token_type,"
+ "access_token_scopes,"
+ "oidc_id_token_value,"
+ "oidc_id_token_issued_at,"
+ "oidc_id_token_expires_at,"
+ "oidc_id_token_metadata,"
+ "refresh_token_value,"
+ "refresh_token_issued_at,"
+ "refresh_token_expires_at,"
+ "refresh_token_metadata";
// @formatter:on
private static final String TABLE_NAME = "oauth2_authorization";
private static final String PK_FILTER = "id = ?";
private static final String UNKNOWN_TOKEN_TYPE_FILTER = "state = ? OR authorization_code_value = ? OR " +
"access_token_value = ? OR refresh_token_value = ?";
private static final String STATE_FILTER = "state = ?";
private static final String AUTHORIZATION_CODE_FILTER = "authorization_code_value = ?";
private static final String ACCESS_TOKEN_FILTER = "access_token_value = ?";
private static final String REFRESH_TOKEN_FILTER = "refresh_token_value = ?";
// @formatter:off
private static final String LOAD_AUTHORIZATION_SQL = "SELECT " + COLUMN_NAMES
+ " FROM " + TABLE_NAME
+ " WHERE ";
// @formatter:on
// @formatter:off
private static final String SAVE_AUTHORIZATION_SQL = "INSERT INTO " + TABLE_NAME
+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
// @formatter:on
// @formatter:off
private static final String UPDATE_AUTHORIZATION_SQL = "UPDATE " + TABLE_NAME
+ " SET registered_client_id = ?, principal_name = ?, authorization_grant_type = ?, attributes = ?, state = ?,"
+ " authorization_code_value = ?, authorization_code_issued_at = ?, authorization_code_expires_at = ?, authorization_code_metadata = ?,"
+ " access_token_value = ?, access_token_issued_at = ?, access_token_expires_at = ?, access_token_metadata = ?, access_token_type = ?, access_token_scopes = ?,"
+ " oidc_id_token_value = ?, oidc_id_token_issued_at = ?, oidc_id_token_expires_at = ?, oidc_id_token_metadata = ?,"
+ " refresh_token_value = ?, refresh_token_issued_at = ?, refresh_token_expires_at = ?, refresh_token_metadata = ?"
+ " WHERE " + PK_FILTER;
// @formatter:on
private static final String REMOVE_AUTHORIZATION_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + PK_FILTER;
private static Map<String, ColumnMetadata> columnMetadataMap;
private final JdbcOperations jdbcOperations;
private final LobHandler lobHandler;
private RowMapper<OAuth2Authorization> authorizationRowMapper;
private Function<OAuth2Authorization, List<SqlParameterValue>> authorizationParametersMapper;
/**
* Constructs a {@code JdbcOAuth2AuthorizationService} using the provided parameters.
*
* @param jdbcOperations the JDBC operations
* @param registeredClientRepository the registered client repository
*/
public JdbcOAuth2AuthorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
this(jdbcOperations, registeredClientRepository, new DefaultLobHandler());
}
/**
* Constructs a {@code JdbcOAuth2AuthorizationService} using the provided parameters.
*
* @param jdbcOperations the JDBC operations
* @param registeredClientRepository the registered client repository
* @param lobHandler the handler for large binary fields and large text fields
*/
public JdbcOAuth2AuthorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository, LobHandler lobHandler) {
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(lobHandler, "lobHandler cannot be null");
this.jdbcOperations = jdbcOperations;
this.lobHandler = lobHandler;
OAuth2AuthorizationRowMapper authorizationRowMapper = new OAuth2AuthorizationRowMapper(registeredClientRepository);
authorizationRowMapper.setLobHandler(lobHandler);
this.authorizationRowMapper = authorizationRowMapper;
this.authorizationParametersMapper = new OAuth2AuthorizationParametersMapper();
initColumnMetadata(jdbcOperations);
}
@Override
public void save(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
OAuth2Authorization existingAuthorization = findById(authorization.getId());
if (existingAuthorization == null) {
insertAuthorization(authorization);
} else {
updateAuthorization(authorization);
}
}
private void updateAuthorization(OAuth2Authorization authorization) {
List<SqlParameterValue> parameters = this.authorizationParametersMapper.apply(authorization);
SqlParameterValue id = parameters.remove(0);
parameters.add(id);
try (LobCreator lobCreator = this.lobHandler.getLobCreator()) {
PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator,
parameters.toArray());
this.jdbcOperations.update(UPDATE_AUTHORIZATION_SQL, pss);
}
}
private void insertAuthorization(OAuth2Authorization authorization) {
List<SqlParameterValue> parameters = this.authorizationParametersMapper.apply(authorization);
try (LobCreator lobCreator = this.lobHandler.getLobCreator()) {
PreparedStatementSetter pss = new LobCreatorArgumentPreparedStatementSetter(lobCreator,
parameters.toArray());
this.jdbcOperations.update(SAVE_AUTHORIZATION_SQL, pss);
}
}
@Override
public void remove(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
SqlParameterValue[] parameters = new SqlParameterValue[] {
new SqlParameterValue(Types.VARCHAR, authorization.getId())
};
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters);
this.jdbcOperations.update(REMOVE_AUTHORIZATION_SQL, pss);
}
@Nullable
@Override
public OAuth2Authorization findById(String id) {
Assert.hasText(id, "id cannot be empty");
List<SqlParameterValue> parameters = new ArrayList<>();
parameters.add(new SqlParameterValue(Types.VARCHAR, id));
return findBy(PK_FILTER, parameters);
}
@Nullable
@Override
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
Assert.hasText(token, "token cannot be empty");
List<SqlParameterValue> parameters = new ArrayList<>();
if (tokenType == null) {
parameters.add(new SqlParameterValue(Types.VARCHAR, token));
parameters.add(mapToSqlParameter("authorization_code_value", token));
parameters.add(mapToSqlParameter("access_token_value", token));
parameters.add(mapToSqlParameter("refresh_token_value", token));
return findBy(UNKNOWN_TOKEN_TYPE_FILTER, parameters);
} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
parameters.add(new SqlParameterValue(Types.VARCHAR, token));
return findBy(STATE_FILTER, parameters);
} else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
parameters.add(mapToSqlParameter("authorization_code_value", token));
return findBy(AUTHORIZATION_CODE_FILTER, parameters);
} else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
parameters.add(mapToSqlParameter("access_token_value", token));
return findBy(ACCESS_TOKEN_FILTER, parameters);
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
parameters.add(mapToSqlParameter("refresh_token_value", token));
return findBy(REFRESH_TOKEN_FILTER, parameters);
}
return null;
}
private OAuth2Authorization findBy(String filter, List<SqlParameterValue> parameters) {
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;
}
}
/**
* Sets the {@link RowMapper} used for mapping the current row in
* {@code java.sql.ResultSet} to {@link OAuth2Authorization}. The default is
* {@link OAuth2AuthorizationRowMapper}.
*
* @param authorizationRowMapper the {@link RowMapper} used for mapping the current
* row in {@code ResultSet} to {@link OAuth2Authorization}
*/
public final void setAuthorizationRowMapper(RowMapper<OAuth2Authorization> authorizationRowMapper) {
Assert.notNull(authorizationRowMapper, "authorizationRowMapper cannot be null");
this.authorizationRowMapper = authorizationRowMapper;
}
/**
* Sets the {@code Function} used for mapping {@link OAuth2Authorization} to
* a {@code List} of {@link SqlParameterValue}. The default is
* {@link OAuth2AuthorizationParametersMapper}.
*
* @param authorizationParametersMapper the {@code Function} used for mapping
* {@link OAuth2Authorization} to a {@code List} of {@link SqlParameterValue}
*/
public final void setAuthorizationParametersMapper(
Function<OAuth2Authorization, List<SqlParameterValue>> authorizationParametersMapper) {
Assert.notNull(authorizationParametersMapper, "authorizationParametersMapper cannot be null");
this.authorizationParametersMapper = authorizationParametersMapper;
}
protected final JdbcOperations getJdbcOperations() {
return this.jdbcOperations;
}
protected final LobHandler getLobHandler() {
return this.lobHandler;
}
protected final RowMapper<OAuth2Authorization> getAuthorizationRowMapper() {
return this.authorizationRowMapper;
}
protected final Function<OAuth2Authorization, List<SqlParameterValue>> getAuthorizationParametersMapper() {
return this.authorizationParametersMapper;
}
/**
* The default {@link RowMapper} that maps the current row in
* {@code java.sql.ResultSet} to {@link OAuth2Authorization}.
*/
public static class OAuth2AuthorizationRowMapper implements RowMapper<OAuth2Authorization> {
private final RegisteredClientRepository registeredClientRepository;
private LobHandler lobHandler = new DefaultLobHandler();
private ObjectMapper objectMapper = new ObjectMapper();
public OAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
this.registeredClientRepository = registeredClientRepository;
ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
this.objectMapper.registerModules(securityModules);
this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
}
@Override
@SuppressWarnings("unchecked")
public OAuth2Authorization mapRow(ResultSet rs, int rowNum) throws SQLException {
String registeredClientId = rs.getString("registered_client_id");
RegisteredClient registeredClient = this.registeredClientRepository.findById(registeredClientId);
if (registeredClient == null) {
throw new DataRetrievalFailureException(
"The RegisteredClient with id '" + registeredClientId + "' was not found in the RegisteredClientRepository.");
}
OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(registeredClient);
String id = rs.getString("id");
String principalName = rs.getString("principal_name");
String authorizationGrantType = rs.getString("authorization_grant_type");
Map<String, Object> attributes = parseMap(getLobValue(rs, "attributes"));
builder.id(id)
.principalName(principalName)
.authorizationGrantType(new AuthorizationGrantType(authorizationGrantType))
.attributes((attrs) -> attrs.putAll(attributes));
String state = rs.getString("state");
if (StringUtils.hasText(state)) {
builder.attribute(OAuth2ParameterNames.STATE, state);
}
Instant tokenIssuedAt;
Instant tokenExpiresAt;
String authorizationCodeValue = getLobValue(rs, "authorization_code_value");
if (StringUtils.hasText(authorizationCodeValue)) {
tokenIssuedAt = rs.getTimestamp("authorization_code_issued_at").toInstant();
tokenExpiresAt = rs.getTimestamp("authorization_code_expires_at").toInstant();
Map<String, Object> authorizationCodeMetadata = parseMap(getLobValue(rs, "authorization_code_metadata"));
OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
authorizationCodeValue, tokenIssuedAt, tokenExpiresAt);
builder.token(authorizationCode, (metadata) -> metadata.putAll(authorizationCodeMetadata));
}
String accessTokenValue = getLobValue(rs, "access_token_value");
if (StringUtils.hasText(accessTokenValue)) {
tokenIssuedAt = rs.getTimestamp("access_token_issued_at").toInstant();
tokenExpiresAt = rs.getTimestamp("access_token_expires_at").toInstant();
Map<String, Object> accessTokenMetadata = parseMap(getLobValue(rs, "access_token_metadata"));
OAuth2AccessToken.TokenType tokenType = null;
if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(rs.getString("access_token_type"))) {
tokenType = OAuth2AccessToken.TokenType.BEARER;
}
Set<String> scopes = Collections.emptySet();
String accessTokenScopes = rs.getString("access_token_scopes");
if (accessTokenScopes != null) {
scopes = StringUtils.commaDelimitedListToSet(accessTokenScopes);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, accessTokenValue, tokenIssuedAt, tokenExpiresAt, scopes);
builder.token(accessToken, (metadata) -> metadata.putAll(accessTokenMetadata));
}
String oidcIdTokenValue = getLobValue(rs, "oidc_id_token_value");
if (StringUtils.hasText(oidcIdTokenValue)) {
tokenIssuedAt = rs.getTimestamp("oidc_id_token_issued_at").toInstant();
tokenExpiresAt = rs.getTimestamp("oidc_id_token_expires_at").toInstant();
Map<String, Object> oidcTokenMetadata = parseMap(getLobValue(rs, "oidc_id_token_metadata"));
OidcIdToken oidcToken = new OidcIdToken(
oidcIdTokenValue, tokenIssuedAt, tokenExpiresAt, (Map<String, Object>) oidcTokenMetadata.get(OAuth2Authorization.Token.CLAIMS_METADATA_NAME));
builder.token(oidcToken, (metadata) -> metadata.putAll(oidcTokenMetadata));
}
String refreshTokenValue = getLobValue(rs, "refresh_token_value");
if (StringUtils.hasText(refreshTokenValue)) {
tokenIssuedAt = rs.getTimestamp("refresh_token_issued_at").toInstant();
tokenExpiresAt = null;
Timestamp refreshTokenExpiresAt = rs.getTimestamp("refresh_token_expires_at");
if (refreshTokenExpiresAt != null) {
tokenExpiresAt = refreshTokenExpiresAt.toInstant();
}
Map<String, Object> refreshTokenMetadata = parseMap(getLobValue(rs, "refresh_token_metadata"));
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(
refreshTokenValue, tokenIssuedAt, tokenExpiresAt);
builder.token(refreshToken, (metadata) -> metadata.putAll(refreshTokenMetadata));
}
return builder.build();
}
private String getLobValue(ResultSet rs, String columnName) throws SQLException {
String columnValue = null;
ColumnMetadata columnMetadata = columnMetadataMap.get(columnName);
if (Types.BLOB == columnMetadata.getDataType()) {
byte[] columnValueBytes = this.lobHandler.getBlobAsBytes(rs, columnName);
if (columnValueBytes != null) {
columnValue = new String(columnValueBytes, StandardCharsets.UTF_8);
}
} else if (Types.CLOB == columnMetadata.getDataType()) {
columnValue = this.lobHandler.getClobAsString(rs, columnName);
} else {
columnValue = rs.getString(columnName);
}
return columnValue;
}
public final void setLobHandler(LobHandler lobHandler) {
Assert.notNull(lobHandler, "lobHandler cannot be null");
this.lobHandler = lobHandler;
}
public final void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "objectMapper cannot be null");
this.objectMapper = objectMapper;
}
protected final RegisteredClientRepository getRegisteredClientRepository() {
return this.registeredClientRepository;
}
protected final LobHandler getLobHandler() {
return this.lobHandler;
}
protected final ObjectMapper getObjectMapper() {
return this.objectMapper;
}
private Map<String, Object> parseMap(String data) {
try {
return this.objectMapper.readValue(data, new TypeReference<Map<String, Object>>() {});
} catch (Exception ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
}
}
/**
* The default {@code Function} that maps {@link OAuth2Authorization} to a
* {@code List} of {@link SqlParameterValue}.
*/
public static class OAuth2AuthorizationParametersMapper implements Function<OAuth2Authorization, List<SqlParameterValue>> {
private ObjectMapper objectMapper = new ObjectMapper();
public OAuth2AuthorizationParametersMapper() {
ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
this.objectMapper.registerModules(securityModules);
this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
}
@Override
public List<SqlParameterValue> apply(OAuth2Authorization authorization) {
List<SqlParameterValue> parameters = new ArrayList<>();
parameters.add(new SqlParameterValue(Types.VARCHAR, authorization.getId()));
parameters.add(new SqlParameterValue(Types.VARCHAR, authorization.getRegisteredClientId()));
parameters.add(new SqlParameterValue(Types.VARCHAR, authorization.getPrincipalName()));
parameters.add(new SqlParameterValue(Types.VARCHAR, authorization.getAuthorizationGrantType().getValue()));
String attributes = writeMap(authorization.getAttributes());
parameters.add(mapToSqlParameter("attributes", attributes));
String state = null;
String authorizationState = authorization.getAttribute(OAuth2ParameterNames.STATE);
if (StringUtils.hasText(authorizationState)) {
state = authorizationState;
}
parameters.add(new SqlParameterValue(Types.VARCHAR, state));
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
authorization.getToken(OAuth2AuthorizationCode.class);
List<SqlParameterValue> authorizationCodeSqlParameters = toSqlParameterList(
"authorization_code_value", "authorization_code_metadata", authorizationCode);
parameters.addAll(authorizationCodeSqlParameters);
OAuth2Authorization.Token<OAuth2AccessToken> accessToken =
authorization.getToken(OAuth2AccessToken.class);
List<SqlParameterValue> accessTokenSqlParameters = toSqlParameterList(
"access_token_value", "access_token_metadata", accessToken);
parameters.addAll(accessTokenSqlParameters);
String accessTokenType = null;
String accessTokenScopes = null;
if (accessToken != null) {
accessTokenType = accessToken.getToken().getTokenType().getValue();
if (!CollectionUtils.isEmpty(accessToken.getToken().getScopes())) {
accessTokenScopes = StringUtils.collectionToDelimitedString(accessToken.getToken().getScopes(), ",");
}
}
parameters.add(new SqlParameterValue(Types.VARCHAR, accessTokenType));
parameters.add(new SqlParameterValue(Types.VARCHAR, accessTokenScopes));
OAuth2Authorization.Token<OidcIdToken> oidcIdToken = authorization.getToken(OidcIdToken.class);
List<SqlParameterValue> oidcIdTokenSqlParameters = toSqlParameterList(
"oidc_id_token_value", "oidc_id_token_metadata", oidcIdToken);
parameters.addAll(oidcIdTokenSqlParameters);
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = authorization.getRefreshToken();
List<SqlParameterValue> refreshTokenSqlParameters = toSqlParameterList(
"refresh_token_value", "refresh_token_metadata", refreshToken);
parameters.addAll(refreshTokenSqlParameters);
return parameters;
}
public final void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "objectMapper cannot be null");
this.objectMapper = objectMapper;
}
protected final ObjectMapper getObjectMapper() {
return this.objectMapper;
}
private <T extends AbstractOAuth2Token> List<SqlParameterValue> toSqlParameterList(
String tokenColumnName, String tokenMetadataColumnName, OAuth2Authorization.Token<T> token) {
List<SqlParameterValue> parameters = new ArrayList<>();
String tokenValue = null;
Timestamp tokenIssuedAt = null;
Timestamp tokenExpiresAt = null;
String metadata = null;
if (token != null) {
tokenValue = token.getToken().getTokenValue();
if (token.getToken().getIssuedAt() != null) {
tokenIssuedAt = Timestamp.from(token.getToken().getIssuedAt());
}
if (token.getToken().getExpiresAt() != null) {
tokenExpiresAt = Timestamp.from(token.getToken().getExpiresAt());
}
metadata = writeMap(token.getMetadata());
}
parameters.add(mapToSqlParameter(tokenColumnName, tokenValue));
parameters.add(new SqlParameterValue(Types.TIMESTAMP, tokenIssuedAt));
parameters.add(new SqlParameterValue(Types.TIMESTAMP, tokenExpiresAt));
parameters.add(mapToSqlParameter(tokenMetadataColumnName, metadata));
return parameters;
}
private String writeMap(Map<String, Object> data) {
try {
return this.objectMapper.writeValueAsString(data);
} catch (Exception ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
}
}
private static final class LobCreatorArgumentPreparedStatementSetter extends ArgumentPreparedStatementSetter {
private final LobCreator lobCreator;
private LobCreatorArgumentPreparedStatementSetter(LobCreator lobCreator, Object[] args) {
super(args);
this.lobCreator = lobCreator;
}
@Override
protected void doSetValue(PreparedStatement ps, int parameterPosition, Object argValue) throws SQLException {
if (argValue instanceof SqlParameterValue) {
SqlParameterValue paramValue = (SqlParameterValue) argValue;
if (paramValue.getSqlType() == Types.BLOB) {
if (paramValue.getValue() != null) {
Assert.isInstanceOf(byte[].class, paramValue.getValue(),
"Value of blob parameter must be byte[]");
}
byte[] valueBytes = (byte[]) paramValue.getValue();
this.lobCreator.setBlobAsBytes(ps, parameterPosition, valueBytes);
return;
}
if (paramValue.getSqlType() == Types.CLOB) {
if (paramValue.getValue() != null) {
Assert.isInstanceOf(String.class, paramValue.getValue(),
"Value of clob parameter must be String");
}
String valueString = (String) paramValue.getValue();
this.lobCreator.setClobAsString(ps, parameterPosition, valueString);
return;
}
}
super.doSetValue(ps, parameterPosition, argValue);
}
}
private static final class ColumnMetadata {
private final String columnName;
private final int dataType;
private ColumnMetadata(String columnName, int dataType) {
this.columnName = columnName;
this.dataType = dataType;
}
private String getColumnName() {
return this.columnName;
}
private int getDataType() {
return this.dataType;
}
}
private static void initColumnMetadata(JdbcOperations jdbcOperations) {
columnMetadataMap = new HashMap<>();
ColumnMetadata columnMetadata;
columnMetadata = getColumnMetadata(jdbcOperations, "attributes", Types.BLOB);
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
columnMetadata = getColumnMetadata(jdbcOperations, "authorization_code_value", Types.BLOB);
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
columnMetadata = getColumnMetadata(jdbcOperations, "authorization_code_metadata", Types.BLOB);
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
columnMetadata = getColumnMetadata(jdbcOperations, "access_token_value", Types.BLOB);
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
columnMetadata = getColumnMetadata(jdbcOperations, "access_token_metadata", Types.BLOB);
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
columnMetadata = getColumnMetadata(jdbcOperations, "oidc_id_token_value", Types.BLOB);
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
columnMetadata = getColumnMetadata(jdbcOperations, "oidc_id_token_metadata", Types.BLOB);
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
columnMetadata = getColumnMetadata(jdbcOperations, "refresh_token_value", Types.BLOB);
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
columnMetadata = getColumnMetadata(jdbcOperations, "refresh_token_metadata", Types.BLOB);
columnMetadataMap.put(columnMetadata.getColumnName(), columnMetadata);
}
private static ColumnMetadata getColumnMetadata(JdbcOperations jdbcOperations, String columnName, int defaultDataType) {
Integer dataType = jdbcOperations.execute((ConnectionCallback<Integer>) conn -> {
DatabaseMetaData databaseMetaData = conn.getMetaData();
ResultSet rs = databaseMetaData.getColumns(null, null, TABLE_NAME, columnName);
if (rs.next()) {
return rs.getInt("DATA_TYPE");
}
// NOTE: (Applies to HSQL)
// When a database object is created with one of the CREATE statements or renamed with the ALTER statement,
// if the name is enclosed in double quotes, the exact name is used as the case-normal form.
// But if it is not enclosed in double quotes,
// the name is converted to uppercase and this uppercase version is stored in the database as the case-normal form.
rs = databaseMetaData.getColumns(null, null, TABLE_NAME.toUpperCase(), columnName.toUpperCase());
if (rs.next()) {
return rs.getInt("DATA_TYPE");
}
return null;
});
return new ColumnMetadata(columnName, dataType != null ? dataType : defaultDataType);
}
private static SqlParameterValue mapToSqlParameter(String columnName, String value) {
ColumnMetadata columnMetadata = columnMetadataMap.get(columnName);
return Types.BLOB == columnMetadata.getDataType() && StringUtils.hasText(value) ?
new SqlParameterValue(Types.BLOB, value.getBytes(StandardCharsets.UTF_8)) :
new SqlParameterValue(columnMetadata.getDataType(), value);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,52 +15,82 @@
*/
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}
* allowing the ability to add, replace, or remove.
*
* @return the {@link JoseHeader.Builder}
*/
public JoseHeader.Builder getHeaders() {
return get(JoseHeader.Builder.class);
}
/**
* Returns the {@link JwtClaimsSet.Builder claims}
* allowing the ability to add, replace, or remove.
*
* @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 +100,43 @@ 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.
*
* @deprecated Use {@link #getHeaders()} instead
* @param headersConsumer a {@code Consumer} of the {@link JoseHeader.Builder headers}
* @return the {@link Builder} for further configuration
*/
@Deprecated
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.
*
* @deprecated Use {@link #getClaims()} instead
* @param claimsConsumer a {@code Consumer} of the {@link JwtClaimsSet.Builder claims}
* @return the {@link Builder} for further configuration
*/
@Deprecated
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

@@ -0,0 +1,220 @@
/*
* 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;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import org.springframework.lang.NonNull;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;
/**
* A representation of an OAuth 2.0 "consent" to an Authorization request, which holds state related to the
* set of {@link #getAuthorities() authorities} granted to a {@link #getRegisteredClientId() client} by the
* {@link #getPrincipalName() resource owner}.
* <p>
* When authorizing access for a given client, the resource owner may only grant a subset of the authorities
* the client requested. The typical use-case is the {@code authorization_code} flow, in which the client
* requests a set of {@code scope}s. The resource owner then selects which scopes they grant to the client.
*
* @author Daniel Garnier-Moiroux
* @since 0.1.2
*/
public final class OAuth2AuthorizationConsent implements Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private static final String AUTHORITIES_SCOPE_PREFIX = "SCOPE_";
private final String registeredClientId;
private final String principalName;
private final Set<GrantedAuthority> authorities;
private OAuth2AuthorizationConsent(String registeredClientId, String principalName, Set<GrantedAuthority> authorities) {
this.registeredClientId = registeredClientId;
this.principalName = principalName;
this.authorities = Collections.unmodifiableSet(authorities);
}
/**
* Returns the identifier for the {@link RegisteredClient#getId() registered client}.
*
* @return the {@link RegisteredClient#getId()}
*/
public String getRegisteredClientId() {
return this.registeredClientId;
}
/**
* Returns the {@code Principal} name of the resource owner (or client).
*
* @return the {@code Principal} name of the resource owner (or client)
*/
public String getPrincipalName() {
return this.principalName;
}
/**
* Returns the {@link GrantedAuthority authorities} granted to the client by the principal.
*
* @return the {@link GrantedAuthority authorities} granted to the client by the principal.
*/
public Set<GrantedAuthority> getAuthorities() {
return this.authorities;
}
/**
* Convenience method for obtaining the {@code scope}s granted to the client by the principal,
* extracted from the {@link #getAuthorities() authorities}.
*
* @return the {@code scope}s granted to the client by the principal.
*/
public Set<String> getScopes() {
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
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
OAuth2AuthorizationConsent that = (OAuth2AuthorizationConsent) obj;
return Objects.equals(this.registeredClientId, that.registeredClientId) &&
Objects.equals(this.principalName, that.principalName) &&
Objects.equals(this.authorities, that.authorities);
}
@Override
public int hashCode() {
return Objects.hash(this.registeredClientId, this.principalName, this.authorities);
}
/**
* Returns a new {@link Builder}, initialized with the values from the provided {@code OAuth2AuthorizationConsent}.
*
* @param authorizationConsent the {@code OAuth2AuthorizationConsent} used for initializing the {@link Builder}
* @return the {@link Builder}
*/
public static Builder from(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
return new Builder(
authorizationConsent.getRegisteredClientId(),
authorizationConsent.getPrincipalName(),
authorizationConsent.getAuthorities()
);
}
/**
* Returns a new {@link Builder}, initialized with the given {@link RegisteredClient#getClientId() registeredClientId}
* and {@code Principal} name.
*
* @param registeredClientId the {@link RegisteredClient#getId()}
* @param principalName the {@code Principal} name
* @return the {@link Builder}
*/
public static Builder withId(@NonNull String registeredClientId, @NonNull String principalName) {
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
return new Builder(registeredClientId, principalName);
}
/**
* A builder for {@link OAuth2AuthorizationConsent}.
*/
public static final class Builder implements Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private final String registeredClientId;
private final String principalName;
private final Set<GrantedAuthority> authorities = new HashSet<>();
private Builder(String registeredClientId, String principalName) {
this(registeredClientId, principalName, Collections.emptySet());
}
private Builder(String registeredClientId, String principalName, Set<GrantedAuthority> authorities) {
this.registeredClientId = registeredClientId;
this.principalName = principalName;
if (!CollectionUtils.isEmpty(authorities)) {
this.authorities.addAll(authorities);
}
}
/**
* Adds a scope to the collection of {@code authorities} in the resulting {@link OAuth2AuthorizationConsent},
* wrapping it in a {@link SimpleGrantedAuthority}, prefixed by {@code SCOPE_}. For example, a
* {@code message.write} scope would be stored as {@code SCOPE_message.write}.
*
* @param scope the scope
* @return the {@code Builder} for further configuration
*/
public Builder scope(String scope) {
authority(new SimpleGrantedAuthority(AUTHORITIES_SCOPE_PREFIX + scope));
return this;
}
/**
* Adds a {@link GrantedAuthority} to the collection of {@code authorities} in the
* resulting {@link OAuth2AuthorizationConsent}.
*
* @param authority the {@link GrantedAuthority}
* @return the {@code Builder} for further configuration
*/
public Builder authority(GrantedAuthority authority) {
this.authorities.add(authority);
return this;
}
/**
* A {@code Consumer} of the {@code authorities}, allowing the ability to add, replace or remove.
*
* @param authoritiesConsumer a {@code Consumer} of the {@code authorities}
* @return the {@code Builder} for further configuration
*/
public Builder authorities(Consumer<Set<GrantedAuthority>> authoritiesConsumer) {
authoritiesConsumer.accept(this.authorities);
return this;
}
/**
* Validate the authorities and build the {@link OAuth2AuthorizationConsent}.
* There must be at least one {@link GrantedAuthority}.
*
* @return the {@link OAuth2AuthorizationConsent}
*/
public OAuth2AuthorizationConsent build() {
Assert.notEmpty(this.authorities, "authorities cannot be empty");
return new OAuth2AuthorizationConsent(this.registeredClientId, this.principalName, this.authorities);
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import java.security.Principal;
/**
* Implementations of this interface are responsible for the management
* of {@link OAuth2AuthorizationConsent OAuth 2.0 Authorization Consent(s)}.
*
* @author Daniel Garnier-Moiroux
* @since 0.1.2
* @see OAuth2AuthorizationConsent
*/
public interface OAuth2AuthorizationConsentService {
/**
* Saves the {@link OAuth2AuthorizationConsent}.
*
* @param authorizationConsent the {@link OAuth2AuthorizationConsent}
*/
void save(OAuth2AuthorizationConsent authorizationConsent);
/**
* Removes the {@link OAuth2AuthorizationConsent}.
*
* @param authorizationConsent the {@link OAuth2AuthorizationConsent}
*/
void remove(OAuth2AuthorizationConsent authorizationConsent);
/**
* Returns the {@link OAuth2AuthorizationConsent} identified by the provided
* {@code registeredClientId} and {@code principalName}, or {@code null} if not found.
*
* @param registeredClientId the identifier for the {@link RegisteredClient}
* @param principalName the name of the {@link Principal}
* @return the {@link OAuth2AuthorizationConsent} if found, otherwise {@code null}
*/
@Nullable
OAuth2AuthorizationConsent findById(String registeredClientId, String principalName);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,81 +27,201 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.context.Context;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
/**
* A context that holds information (to be) associated to an OAuth 2.0 Token
* and is used by an {@link OAuth2TokenGenerator} and {@link OAuth2TokenCustomizer}.
*
* @author Joe Grandja
* @since 0.1.0
* @see Context
* @see OAuth2TokenGenerator
* @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 ProviderContext provider context}.
*
* @return the {@link ProviderContext}
* @since 0.2.3
*/
default ProviderContext getProviderContext() {
return get(ProviderContext.class);
}
/**
* 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 ProviderContext provider context}.
*
* @param providerContext the {@link ProviderContext}
* @return the {@link AbstractBuilder} for further configuration
* @since 0.2.3
*/
public B providerContext(ProviderContext providerContext) {
return put(ProviderContext.class, providerContext);
}
/**
* 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 +229,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 +246,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

@@ -0,0 +1,131 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
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.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} implementation used for OAuth 2.0 Client Authentication,
* which authenticates the {@link OAuth2ParameterNames#CLIENT_SECRET client_secret} parameter.
*
* @author Patryk Kostrzewa
* @author Joe Grandja
* @since 0.2.3
* @see AuthenticationProvider
* @see OAuth2ClientAuthenticationToken
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @see PasswordEncoder
*/
public final class ClientSecretAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
private final RegisteredClientRepository registeredClientRepository;
private final CodeVerifierAuthenticator codeVerifierAuthenticator;
private PasswordEncoder passwordEncoder;
/**
* Constructs a {@code ClientSecretAuthenticationProvider} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
*/
public ClientSecretAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.registeredClientRepository = registeredClientRepository;
this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
/**
* Sets the {@link PasswordEncoder} used to validate
* the {@link RegisteredClient#getClientSecret() client secret}.
* If not set, the client secret will be compared using
* {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}.
*
* @param passwordEncoder the {@link PasswordEncoder} used to validate the client secret
*/
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2ClientAuthenticationToken clientAuthentication =
(OAuth2ClientAuthenticationToken) authentication;
if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getClientAuthenticationMethod()) &&
!ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getClientAuthenticationMethod())) {
return null;
}
String clientId = clientAuthentication.getPrincipal().toString();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
}
if (!registeredClient.getClientAuthenticationMethods().contains(
clientAuthentication.getClientAuthenticationMethod())) {
throwInvalidClient("authentication_method");
}
if (clientAuthentication.getCredentials() == null) {
throwInvalidClient("credentials");
}
String clientSecret = clientAuthentication.getCredentials().toString();
if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
}
// Validate the "code_verifier" parameter for the confidential client, if available
this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
return new OAuth2ClientAuthenticationToken(registeredClient,
clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
}
private static void throwInvalidClient(String parameterName) {
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_CLIENT,
"Client authentication failed: " + parameterName,
ERROR_URI
);
throw new OAuth2AuthenticationException(error);
}
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Map;
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.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenType;
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.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* An authenticator used for OAuth 2.0 Client Authentication,
* which authenticates the {@link PkceParameterNames#CODE_VERIFIER code_verifier} parameter.
*
* @author Daniel Garnier-Moiroux
* @author Joe Grandja
* @since 0.2.3
* @see OAuth2ClientAuthenticationToken
* @see OAuth2AuthorizationService
*/
final class CodeVerifierAuthenticator {
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
private final OAuth2AuthorizationService authorizationService;
CodeVerifierAuthenticator(OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.authorizationService = authorizationService;
}
void authenticateRequired(OAuth2ClientAuthenticationToken clientAuthentication,
RegisteredClient registeredClient) {
if (!authenticate(clientAuthentication, registeredClient)) {
throwInvalidGrant(PkceParameterNames.CODE_VERIFIER);
}
}
void authenticateIfAvailable(OAuth2ClientAuthenticationToken clientAuthentication,
RegisteredClient registeredClient) {
authenticate(clientAuthentication, registeredClient);
}
private boolean authenticate(OAuth2ClientAuthenticationToken clientAuthentication,
RegisteredClient registeredClient) {
Map<String, Object> parameters = clientAuthentication.getAdditionalParameters();
if (!authorizationCodeGrant(parameters)) {
return false;
}
OAuth2Authorization authorization = this.authorizationService.findByToken(
(String) parameters.get(OAuth2ParameterNames.CODE),
AUTHORIZATION_CODE_TOKEN_TYPE);
if (authorization == null) {
throwInvalidGrant(OAuth2ParameterNames.CODE);
}
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
OAuth2AuthorizationRequest.class.getName());
String codeChallenge = (String) authorizationRequest.getAdditionalParameters()
.get(PkceParameterNames.CODE_CHALLENGE);
if (!StringUtils.hasText(codeChallenge)) {
if (registeredClient.getClientSettings().isRequireProofKey()) {
throwInvalidGrant(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)) {
throwInvalidGrant(PkceParameterNames.CODE_VERIFIER);
}
return true;
}
private static boolean authorizationCodeGrant(Map<String, Object> parameters) {
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
parameters.get(OAuth2ParameterNames.GRANT_TYPE)) &&
parameters.get(OAuth2ParameterNames.CODE) != null;
}
private static boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
if (!StringUtils.hasText(codeVerifier)) {
return false;
} else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) {
return codeVerifier.equals(codeChallenge);
} else if ("S256".equals(codeChallengeMethod)) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
return encodedVerifier.equals(codeChallenge);
} catch (NoSuchAlgorithmException ex) {
// It is unlikely that SHA-256 is not available on the server. If it is not available,
// there will likely be bigger issues as well. We default to SERVER_ERROR.
}
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.SERVER_ERROR);
}
private static void throwInvalidGrant(String parameterName) {
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_GRANT,
"Client authentication failed: " + parameterName,
null
);
throw new OAuth2AuthenticationException(error);
}
}

View File

@@ -0,0 +1,266 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
/**
* An {@link AuthenticationProvider} implementation used for OAuth 2.0 Client Authentication,
* which authenticates the (JWT) {@link OAuth2ParameterNames#CLIENT_ASSERTION client_assertion} parameter.
*
* @author Rafal Lewczuk
* @author Joe Grandja
* @since 0.2.3
* @see AuthenticationProvider
* @see OAuth2ClientAuthenticationToken
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
*/
public final class JwtClientAssertionAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD =
new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
private final RegisteredClientRepository registeredClientRepository;
private final CodeVerifierAuthenticator codeVerifierAuthenticator;
private final JwtClientAssertionDecoderFactory jwtClientAssertionDecoderFactory;
/**
* Constructs a {@code JwtClientAssertionAuthenticationProvider} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
*/
public JwtClientAssertionAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.registeredClientRepository = registeredClientRepository;
this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
this.jwtClientAssertionDecoderFactory = new JwtClientAssertionDecoderFactory();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2ClientAuthenticationToken clientAuthentication =
(OAuth2ClientAuthenticationToken) authentication;
if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
return null;
}
String clientId = clientAuthentication.getPrincipal().toString();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
}
if (!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT) &&
!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
throwInvalidClient("authentication_method");
}
if (clientAuthentication.getCredentials() == null) {
throwInvalidClient("credentials");
}
Jwt jwtAssertion = null;
JwtDecoder jwtDecoder = this.jwtClientAssertionDecoderFactory.createDecoder(registeredClient);
try {
jwtAssertion = jwtDecoder.decode(clientAuthentication.getCredentials().toString());
} catch (JwtException ex) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION, ex);
}
// Validate the "code_verifier" parameter for the confidential client, if available
this.codeVerifierAuthenticator.authenticateIfAvailable(clientAuthentication, registeredClient);
ClientAuthenticationMethod clientAuthenticationMethod =
registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm() instanceof SignatureAlgorithm ?
ClientAuthenticationMethod.PRIVATE_KEY_JWT :
ClientAuthenticationMethod.CLIENT_SECRET_JWT;
return new OAuth2ClientAuthenticationToken(registeredClient, clientAuthenticationMethod, jwtAssertion);
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
}
private static void throwInvalidClient(String parameterName) {
throwInvalidClient(parameterName, null);
}
private static void throwInvalidClient(String parameterName, Throwable cause) {
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_CLIENT,
"Client authentication failed: " + parameterName,
ERROR_URI
);
throw new OAuth2AuthenticationException(error, error.toString(), cause);
}
private static class JwtClientAssertionDecoderFactory implements JwtDecoderFactory<RegisteredClient> {
private static final String JWT_CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7523#section-3";
private static final Map<JwsAlgorithm, String> JCA_ALGORITHM_MAPPINGS;
static {
Map<JwsAlgorithm, String> mappings = new HashMap<>();
mappings.put(MacAlgorithm.HS256, "HmacSHA256");
mappings.put(MacAlgorithm.HS384, "HmacSHA384");
mappings.put(MacAlgorithm.HS512, "HmacSHA512");
JCA_ALGORITHM_MAPPINGS = Collections.unmodifiableMap(mappings);
}
private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
@Override
public JwtDecoder createDecoder(RegisteredClient registeredClient) {
Assert.notNull(registeredClient, "registeredClient cannot be null");
return this.jwtDecoders.computeIfAbsent(registeredClient.getId(), (key) -> {
NimbusJwtDecoder jwtDecoder = buildDecoder(registeredClient);
jwtDecoder.setJwtValidator(createJwtValidator(registeredClient));
return jwtDecoder;
});
}
private static NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) {
JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm();
if (jwsAlgorithm instanceof SignatureAlgorithm) {
String jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl();
if (!StringUtils.hasText(jwkSetUrl)) {
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
"Failed to find a Signature Verifier for Client: '"
+ registeredClient.getId()
+ "'. Check to ensure you have configured the JWK Set URL.",
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error);
}
return NimbusJwtDecoder.withJwkSetUri(jwkSetUrl).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();
}
if (jwsAlgorithm instanceof MacAlgorithm) {
String clientSecret = registeredClient.getClientSecret();
if (!StringUtils.hasText(clientSecret)) {
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
"Failed to find a Signature Verifier for Client: '"
+ registeredClient.getId()
+ "'. Check to ensure you have configured the client secret.",
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error);
}
SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
}
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
"Failed to find a Signature Verifier for Client: '"
+ registeredClient.getId()
+ "'. Check to ensure you have configured a valid JWS Algorithm: '" + jwsAlgorithm + "'.",
JWT_CLIENT_AUTHENTICATION_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error);
}
private static OAuth2TokenValidator<Jwt> createJwtValidator(RegisteredClient registeredClient) {
String clientId = registeredClient.getClientId();
return new DelegatingOAuth2TokenValidator<>(
new JwtClaimValidator<>(JwtClaimNames.ISS, clientId::equals),
new JwtClaimValidator<>(JwtClaimNames.SUB, clientId::equals),
new JwtClaimValidator<>(JwtClaimNames.AUD, containsProviderAudience()),
new JwtClaimValidator<>(JwtClaimNames.EXP, Objects::nonNull),
new JwtTimestampValidator()
);
}
private static Predicate<List<String>> containsProviderAudience() {
return (audienceClaim) -> {
if (CollectionUtils.isEmpty(audienceClaim)) {
return false;
}
List<String> providerAudience = getProviderAudience();
for (String audience : audienceClaim) {
if (providerAudience.contains(audience)) {
return true;
}
}
return false;
};
}
private static List<String> getProviderAudience() {
ProviderContext providerContext = ProviderContextHolder.getProviderContext();
if (!StringUtils.hasText(providerContext.getIssuer())) {
return Collections.emptyList();
}
ProviderSettings providerSettings = providerContext.getProviderSettings();
List<String> providerAudience = new ArrayList<>();
providerAudience.add(providerContext.getIssuer());
providerAudience.add(asUrl(providerContext.getIssuer(), providerSettings.getTokenEndpoint()));
providerAudience.add(asUrl(providerContext.getIssuer(), providerSettings.getTokenIntrospectionEndpoint()));
providerAudience.add(asUrl(providerContext.getIssuer(), providerSettings.getTokenRevocationEndpoint()));
return providerAudience;
}
private static String asUrl(String issuer, String endpoint) {
return UriComponentsBuilder.fromUriString(issuer).path(endpoint).build().toUriString();
}
}
}

View File

@@ -1,101 +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.server.authorization.authentication;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
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.core.oidc.IdTokenClaimNames;
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;
/**
* Utility methods used by the {@link AuthenticationProvider}'s when issuing {@link Jwt}'s.
*
* @author Joe Grandja
* @since 0.1.0
*/
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().accessTokenTimeToLive());
// @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;
}
static JwtClaimsSet.Builder idTokenClaims(RegisteredClient registeredClient,
String issuer, String subject, String nonce) {
Instant issuedAt = Instant.now();
// TODO Allow configuration for ID Token time-to-live
Instant expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES);
// @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)
.claim(IdTokenClaimNames.AZP, registeredClient.getClientId());
if (StringUtils.hasText(nonce)) {
claimsBuilder.claim(IdTokenClaimNames.NONCE, nonce);
}
// TODO Add 'auth_time' claim
// @formatter:on
return claimsBuilder;
}
}

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

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,38 +16,48 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.security.Principal;
import java.time.Instant;
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.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClaimAccessor;
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.OAuth2AuthorizationCode;
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.OAuth2Token;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
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.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
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.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -61,44 +71,89 @@ import static org.springframework.security.oauth2.server.authorization.authentic
* @since 0.0.1
* @see OAuth2AuthorizationCodeAuthenticationToken
* @see OAuth2AccessTokenAuthenticationToken
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
* @see OAuth2AuthorizationService
* @see JwtEncoder
* @see OAuth2TokenCustomizer
* @see JwtEncodingContext
* @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>
* @see OAuth2TokenGenerator
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
*/
public class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
public final class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
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 final OAuth2AuthorizationService authorizationService;
private final JwtEncoder jwtEncoder;
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
private ProviderSettings providerSettings;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
// TODO Remove after removing @Deprecated OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService, JwtEncoder)
private JwtGenerator jwtGenerator;
@Deprecated
private Supplier<String> refreshTokenGenerator;
/**
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the provided parameters.
*
* @deprecated Use {@link #OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService, OAuth2TokenGenerator)} instead
* @param authorizationService the authorization service
* @param jwtEncoder the jwt encoder
*/
@Deprecated
public OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
this.authorizationService = authorizationService;
this.jwtGenerator = new JwtGenerator(jwtEncoder);
this.tokenGenerator = new DelegatingOAuth2TokenGenerator(this.jwtGenerator,
new OAuth2AccessTokenGenerator(), new OAuth2RefreshTokenGenerator());
}
/**
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the provided parameters.
*
* @param authorizationService the authorization service
* @param jwtEncoder the jwt encoder
* @param tokenGenerator the token generator
* @since 0.2.3
*/
public OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder) {
public OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.jwtEncoder = jwtEncoder;
this.tokenGenerator = tokenGenerator;
}
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}.
*
* @deprecated Use {@link JwtGenerator#setJwtCustomizer(OAuth2TokenCustomizer)} instead
* @param jwtCustomizer the {@link OAuth2TokenCustomizer} that customizes the headers and/or claims for the generated {@code Jwt}
*/
@Deprecated
public void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
this.jwtCustomizer = jwtCustomizer;
if (this.jwtGenerator != null) {
this.jwtGenerator.setJwtCustomizer(jwtCustomizer);
}
}
@Autowired(required = false)
/**
* Sets the {@code Supplier<String>} that generates the value for the {@link OAuth2RefreshToken}.
*
* @deprecated Use {@link OAuth2RefreshTokenGenerator} instead
* @param refreshTokenGenerator the {@code Supplier<String>} that generates the value for the {@link OAuth2RefreshToken}
*/
@Deprecated
public void setRefreshTokenGenerator(Supplier<String> refreshTokenGenerator) {
Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null");
this.refreshTokenGenerator = refreshTokenGenerator;
}
@Deprecated
protected void setProviderSettings(ProviderSettings providerSettings) {
this.providerSettings = providerSettings;
}
@Override
@@ -113,7 +168,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);
@@ -127,107 +182,91 @@ 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.isInvalidated()) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
if (!authorizationCode.isActive()) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null;
Set<String> authorizedScopes = authorization.getAttribute(
OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME);
JoseHeader.Builder headersBuilder = JwtUtils.headers();
JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(
registeredClient, issuer, authorization.getPrincipalName(),
authorizedScopes);
// @formatter:off
JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(authorization.getAttribute(Principal.class.getName()))
.providerContext(ProviderContextHolder.getProviderContext())
.authorization(authorization)
.authorizedScopes(authorizedScopes)
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.authorizedScopes(authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME))
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrant(authorizationCodeAuthentication)
.build();
.authorizationGrant(authorizationCodeAuthentication);
// @formatter:on
this.jwtCustomizer.customize(context);
JoseHeader headers = context.getHeaders().build();
JwtClaimsSet claims = context.getClaims().build();
Jwt jwtAccessToken = this.jwtEncoder.encode(headers, claims);
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(),
jwtAccessToken.getExpiresAt(), authorizedScopes);
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
} else {
authorizationBuilder.accessToken(accessToken);
}
// ----- Refresh token -----
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
refreshToken = OAuth2RefreshTokenAuthenticationProvider.generateRefreshToken(
registeredClient.getTokenSettings().refreshTokenTimeToLive());
}
Jwt jwtIdToken = null;
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) {
String nonce = (String) authorizationRequest.getAdditionalParameters().get(OidcParameterNames.NONCE);
headersBuilder = JwtUtils.headers();
claimsBuilder = JwtUtils.idTokenClaims(
registeredClient, issuer, authorization.getPrincipalName(), nonce);
// @formatter:off
context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
.registeredClient(registeredClient)
.principal(authorization.getAttribute(Principal.class.getName()))
.authorization(authorization)
.authorizedScopes(authorizedScopes)
.tokenType(ID_TOKEN_TOKEN_TYPE)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrant(authorizationCodeAuthentication)
.build();
// @formatter:on
this.jwtCustomizer.customize(context);
headers = context.getHeaders().build();
claims = context.getClaims().build();
jwtIdToken = this.jwtEncoder.encode(headers, claims);
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// Do not issue refresh token to public client
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
if (this.refreshTokenGenerator != null) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getRefreshTokenTimeToLive());
refreshToken = new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt);
} else {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
}
authorizationBuilder.refreshToken(refreshToken);
}
// ----- ID token -----
OidcIdToken idToken;
if (jwtIdToken != null) {
idToken = new OidcIdToken(jwtIdToken.getTokenValue(), jwtIdToken.getIssuedAt(),
jwtIdToken.getExpiresAt(), jwtIdToken.getClaims());
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) {
tokenContext = tokenContextBuilder.tokenType(ID_TOKEN_TOKEN_TYPE).build();
OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedIdToken instanceof Jwt)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the ID token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
authorizationBuilder.token(idToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
} else {
idToken = null;
}
// @formatter:off
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization)
.token(accessToken,
(metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, jwtAccessToken.getClaims())
);
if (refreshToken != null) {
authorizationBuilder.refreshToken(refreshToken);
}
if (idToken != null) {
authorizationBuilder
.token(idToken,
(metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
}
authorization = authorizationBuilder.build();
// @formatter:on
// Invalidate the authorization code as it can only be used once
authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken());

View File

@@ -0,0 +1,70 @@
/*
* 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 org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
/**
* This exception is thrown by {@link OAuth2AuthorizationCodeRequestAuthenticationProvider}
* when an attempt to authenticate the OAuth 2.0 Authorization Request (or Consent) fails.
*
* @author Joe Grandja
* @since 0.1.2
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
*/
public class OAuth2AuthorizationCodeRequestAuthenticationException extends OAuth2AuthenticationException {
private final OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication;
/**
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationException} using the provided parameters.
*
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param authorizationCodeRequestAuthentication the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent)
*/
public OAuth2AuthorizationCodeRequestAuthenticationException(OAuth2Error error,
@Nullable OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
super(error);
this.authorizationCodeRequestAuthentication = authorizationCodeRequestAuthentication;
}
/**
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationException} using the provided parameters.
*
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param cause the root cause
* @param authorizationCodeRequestAuthentication the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent)
*/
public OAuth2AuthorizationCodeRequestAuthenticationException(OAuth2Error error, Throwable cause,
@Nullable OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
super(error, cause);
this.authorizationCodeRequestAuthentication = authorizationCodeRequestAuthentication;
}
/**
* Returns the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent), or {@code null} if not available.
*
* @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
*/
@Nullable
public OAuth2AuthorizationCodeRequestAuthenticationToken getAuthorizationCodeRequestAuthentication() {
return this.authorizationCodeRequestAuthentication;
}
}

View File

@@ -0,0 +1,706 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.security.Principal;
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.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.springframework.lang.Nullable;
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.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
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.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request (and Consent)
* used in the Authorization Code Grant.
*
* @author Joe Grandja
* @author Steve Riesenberg
* @since 0.1.2
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
* @see OAuth2AuthorizationCodeAuthenticationProvider
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @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 final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
new Base64StringKeyGenerator(Base64.getUrlEncoder());
private static final Function<String, OAuth2AuthenticationValidator> DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER =
createDefaultAuthenticationValidatorResolver();
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2AuthorizationConsentService authorizationConsentService;
@Deprecated
private Supplier<String> authorizationCodeSupplier;
private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
private Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER;
private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
/**
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
* @param authorizationConsentService the authorization consent service
*/
public OAuth2AuthorizationCodeRequestAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService, OAuth2AuthorizationConsentService authorizationConsentService) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
this.registeredClientRepository = registeredClientRepository;
this.authorizationService = authorizationService;
this.authorizationConsentService = authorizationConsentService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
return authorizationCodeRequestAuthentication.isConsent() ?
authenticateAuthorizationConsent(authentication) :
authenticateAuthorizationRequest(authentication);
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* Sets the {@code Supplier<String>} that generates the value for the {@link OAuth2AuthorizationCode}.
*
* @deprecated Use {@link #setAuthorizationCodeGenerator(OAuth2TokenGenerator)} instead
* @param authorizationCodeGenerator the {@code Supplier<String>} that generates the value for the {@link OAuth2AuthorizationCode}
*/
@Deprecated
public void setAuthorizationCodeGenerator(Supplier<String> authorizationCodeGenerator) {
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
this.authorizationCodeSupplier = authorizationCodeGenerator;
}
/**
* Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
*
* @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
* @since 0.2.3
*/
public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
this.authorizationCodeGenerator = authorizationCodeGenerator;
}
/**
* Sets the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter.
*
* <p>
* The following OAuth 2.0 Authorization Request parameters are supported:
* <ol>
* <li>{@link OAuth2ParameterNames#REDIRECT_URI}</li>
* <li>{@link OAuth2ParameterNames#SCOPE}</li>
* </ol>
*
* <p>
* <b>NOTE:</b> The resolved {@link OAuth2AuthenticationValidator} MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
*
* @param authenticationValidatorResolver the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter
*/
public void setAuthenticationValidatorResolver(Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver) {
Assert.notNull(authenticationValidatorResolver, "authenticationValidatorResolver cannot be null");
this.authenticationValidatorResolver = authenticationValidatorResolver;
}
/**
* Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationConsentAuthenticationContext}
* containing an {@link OAuth2AuthorizationConsent.Builder} and additional context information.
*
* <p>
* The following context attributes are available:
* <ul>
* <li>The {@link OAuth2AuthorizationConsent.Builder} used to build the authorization consent
* prior to {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}.</li>
* <li>The {@link Authentication} of type
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.</li>
* <li>The {@link RegisteredClient} associated with the authorization request.</li>
* <li>The {@link OAuth2Authorization} associated with the state token presented in the
* authorization consent request.</li>
* <li>The {@link OAuth2AuthorizationRequest} associated with the authorization consent request.</li>
* </ul>
*
* @param authorizationConsentCustomizer the {@code Consumer} providing access to the
* {@link OAuth2AuthorizationConsentAuthenticationContext} containing an {@link OAuth2AuthorizationConsent.Builder}
*/
public void setAuthorizationConsentCustomizer(Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer) {
Assert.notNull(authorizationConsentCustomizer, "authorizationConsentCustomizer cannot be null");
this.authorizationConsentCustomizer = authorizationConsentCustomizer;
}
private Authentication authenticateAuthorizationRequest(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
authorizationCodeRequestAuthentication.getClientId());
if (registeredClient == null) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
authorizationCodeRequestAuthentication, null);
}
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);
}
OAuth2AuthenticationValidator scopeValidator = resolveAuthenticationValidator(OAuth2ParameterNames.SCOPE);
scopeValidator.validate(authenticationContext);
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE);
if (StringUtils.hasText(codeChallenge)) {
String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD);
if (StringUtils.hasText(codeChallengeMethod)) {
if (!"S256".equals(codeChallengeMethod) && !"plain".equals(codeChallengeMethod)) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
authorizationCodeRequestAuthentication, registeredClient, null);
}
}
} else if (registeredClient.getClientSettings().isRequireProofKey()) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
authorizationCodeRequestAuthentication, registeredClient, null);
}
// ---------------
// The request is valid - ensure the resource owner is authenticated
// ---------------
Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
if (!isPrincipalAuthenticated(principal)) {
// Return the authorization request as-is where isAuthenticated() is false
return authorizationCodeRequestAuthentication;
}
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
.clientId(registeredClient.getClientId())
.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
.scopes(authorizationCodeRequestAuthentication.getScopes())
.state(authorizationCodeRequestAuthentication.getState())
.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
.build();
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
registeredClient.getId(), principal.getName());
if (requireAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {
String state = DEFAULT_STATE_GENERATOR.generateKey();
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
.attribute(OAuth2ParameterNames.STATE, state)
.build();
this.authorizationService.save(authorization);
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
currentAuthorizationConsent.getScopes() : null;
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
.authorizationUri(authorizationRequest.getAuthorizationUri())
.scopes(currentAuthorizedScopes)
.state(state)
.consentRequired(true)
.build();
}
OAuth2AuthorizationCode authorizationCode;
if (this.authorizationCodeSupplier != null) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
authorizationCode = new OAuth2AuthorizationCode(this.authorizationCodeSupplier.get(), issuedAt, expiresAt);
} else {
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
authorizationCodeRequestAuthentication, registeredClient, null, authorizationRequest.getScopes());
authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
if (authorizationCode == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the authorization code.", ERROR_URI);
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
}
}
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
.token(authorizationCode)
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes())
.build();
this.authorizationService.save(authorization);
String redirectUri = authorizationRequest.getRedirectUri();
if (!StringUtils.hasText(redirectUri)) {
redirectUri = registeredClient.getRedirectUris().iterator().next();
}
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
.authorizationUri(authorizationRequest.getAuthorizationUri())
.redirectUri(redirectUri)
.scopes(authorizationRequest.getScopes())
.state(authorizationRequest.getState())
.authorizationCode(authorizationCode)
.build();
}
private OAuth2AuthenticationValidator resolveAuthenticationValidator(String parameterName) {
OAuth2AuthenticationValidator authenticationValidator = this.authenticationValidatorResolver.apply(parameterName);
return authenticationValidator != null ?
authenticationValidator :
DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER.apply(parameterName);
}
private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
OAuth2Authorization authorization = this.authorizationService.findByToken(
authorizationCodeRequestAuthentication.getState(), STATE_TOKEN_TYPE);
if (authorization == null) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
authorizationCodeRequestAuthentication, null, null);
}
// The 'in-flight' authorization must be associated to the current principal
Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
authorizationCodeRequestAuthentication, null, null);
}
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
authorizationCodeRequestAuthentication.getClientId());
if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
authorizationCodeRequestAuthentication, registeredClient);
}
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
Set<String> requestedScopes = authorizationRequest.getScopes();
Set<String> authorizedScopes = new HashSet<>(authorizationCodeRequestAuthentication.getScopes());
if (!requestedScopes.containsAll(authorizedScopes)) {
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
}
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
authorization.getRegisteredClientId(), authorization.getPrincipalName());
Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
currentAuthorizationConsent.getScopes() : Collections.emptySet();
if (!currentAuthorizedScopes.isEmpty()) {
for (String requestedScope : requestedScopes) {
if (currentAuthorizedScopes.contains(requestedScope)) {
authorizedScopes.add(requestedScope);
}
}
}
if (!authorizedScopes.isEmpty() && requestedScopes.contains(OidcScopes.OPENID)) {
// 'openid' scope is auto-approved as it does not require consent
authorizedScopes.add(OidcScopes.OPENID);
}
OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
if (currentAuthorizationConsent != null) {
authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
} else {
authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
authorization.getRegisteredClientId(), authorization.getPrincipalName());
}
authorizedScopes.forEach(authorizationConsentBuilder::scope);
if (this.authorizationConsentCustomizer != null) {
// @formatter:off
OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =
OAuth2AuthorizationConsentAuthenticationContext.with(authorizationCodeRequestAuthentication)
.authorizationConsent(authorizationConsentBuilder)
.registeredClient(registeredClient)
.authorization(authorization)
.authorizationRequest(authorizationRequest)
.build();
// @formatter:on
this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
}
Set<GrantedAuthority> authorities = new HashSet<>();
authorizationConsentBuilder.authorities(authorities::addAll);
if (authorities.isEmpty()) {
// Authorization consent denied (or revoked)
if (currentAuthorizationConsent != null) {
this.authorizationConsentService.remove(currentAuthorizationConsent);
}
this.authorizationService.remove(authorization);
throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
}
OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
if (!authorizationConsent.equals(currentAuthorizationConsent)) {
this.authorizationConsentService.save(authorizationConsent);
}
OAuth2AuthorizationCode authorizationCode;
if (this.authorizationCodeSupplier != null) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
authorizationCode = new OAuth2AuthorizationCode(this.authorizationCodeSupplier.get(), issuedAt, expiresAt);
} else {
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
authorizationCodeRequestAuthentication, registeredClient, authorization, authorizedScopes);
authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
if (authorizationCode == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the authorization code.", ERROR_URI);
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
}
}
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
.token(authorizationCode)
.attributes(attrs -> {
attrs.remove(OAuth2ParameterNames.STATE);
attrs.put(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
})
.build();
this.authorizationService.save(updatedAuthorization);
String redirectUri = authorizationRequest.getRedirectUri();
if (!StringUtils.hasText(redirectUri)) {
redirectUri = registeredClient.getRedirectUris().iterator().next();
}
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
.authorizationUri(authorizationRequest.getAuthorizationUri())
.redirectUri(redirectUri)
.scopes(authorizedScopes)
.state(authorizationRequest.getState())
.authorizationCode(authorizationCode)
.build();
}
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)
.principalName(principal.getName())
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.attribute(Principal.class.getName(), principal)
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);
}
private static OAuth2TokenContext createAuthorizationCodeTokenContext(
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
RegisteredClient registeredClient, OAuth2Authorization authorization, Set<String> authorizedScopes) {
// @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal((Authentication) authorizationCodeRequestAuthentication.getPrincipal())
.providerContext(ProviderContextHolder.getProviderContext())
.tokenType(new OAuth2TokenType(OAuth2ParameterNames.CODE))
.authorizedScopes(authorizedScopes)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrant(authorizationCodeRequestAuthentication);
// @formatter:on
if (authorization != null) {
tokenContextBuilder.authorization(authorization);
}
return tokenContextBuilder.build();
}
private static boolean requireAuthorizationConsent(RegisteredClient registeredClient,
OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {
if (!registeredClient.getClientSettings().isRequireAuthorizationConsent()) {
return false;
}
// 'openid' scope does not require consent
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID) &&
authorizationRequest.getScopes().size() == 1) {
return false;
}
if (authorizationConsent != null &&
authorizationConsent.getScopes().containsAll(authorizationRequest.getScopes())) {
return false;
}
return true;
}
private static boolean isValidRedirectUri(String requestedRedirectUri, RegisteredClient registeredClient) {
UriComponents requestedRedirect;
try {
requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build();
if (requestedRedirect.getFragment() != null) {
return false;
}
} catch (Exception ex) {
return false;
}
String requestedRedirectHost = requestedRedirect.getHost();
if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) {
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-9.7.1
// While redirect URIs using localhost (i.e.,
// "http://localhost:{port}/{path}") function similarly to loopback IP
// redirects described in Section 10.3.3, the use of "localhost" is NOT RECOMMENDED.
return false;
}
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.
return registeredClient.getRedirectUris().contains(requestedRedirectUri);
}
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-10.3.3
// The authorization server MUST allow any port to be specified at the
// time of the request for loopback IP redirect URIs, to accommodate
// clients that obtain an available ephemeral port from the operating
// system at the time of the request.
for (String registeredRedirectUri : registeredClient.getRedirectUris()) {
UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri);
registeredRedirect.port(requestedRedirect.getPort());
if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) {
return true;
}
}
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()) &&
principal.isAuthenticated();
}
private static void throwError(String errorCode, String parameterName,
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
RegisteredClient registeredClient) {
throwError(errorCode, parameterName, authorizationCodeRequestAuthentication, registeredClient, null);
}
private static void throwError(String errorCode, String parameterName,
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
throwError(errorCode, parameterName, ERROR_URI,
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
}
private static void throwError(String errorCode, String parameterName, String errorUri,
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
boolean redirectOnError = true;
if (errorCode.equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
parameterName.equals(OAuth2ParameterNames.REDIRECT_URI) ||
parameterName.equals(OAuth2ParameterNames.STATE))) {
redirectOnError = false;
}
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = authorizationCodeRequestAuthentication;
if (redirectOnError && !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
String state = authorizationCodeRequestAuthentication.isConsent() && authorizationRequest != null ?
authorizationRequest.getState() : authorizationCodeRequestAuthentication.getState();
authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
.redirectUri(redirectUri)
.state(state)
.build();
authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
} else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
.redirectUri(null) // Prevent redirects
.build();
authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
}
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
}
private static String resolveRedirectUri(OAuth2AuthorizationRequest authorizationRequest, RegisteredClient registeredClient) {
if (authorizationRequest != null && StringUtils.hasText(authorizationRequest.getRedirectUri())) {
return authorizationRequest.getRedirectUri();
}
if (registeredClient != null) {
return registeredClient.getRedirectUris().iterator().next();
}
return null;
}
private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
return OAuth2AuthorizationCodeRequestAuthenticationToken.with(authorizationCodeRequestAuthentication.getClientId(), (Authentication) authorizationCodeRequestAuthentication.getPrincipal())
.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
.scopes(authorizationCodeRequestAuthentication.getScopes())
.state(authorizationCodeRequestAuthentication.getState())
.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
}
private static class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator<OAuth2AuthorizationCode> {
private final StringKeyGenerator authorizationCodeGenerator =
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
@Nullable
@Override
public OAuth2AuthorizationCode generate(OAuth2TokenContext context) {
if (context.getTokenType() == null ||
!OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) {
return null;
}
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt);
}
}
private static class DefaultRedirectUriOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
@Override
public void validate(OAuth2AuthenticationContext authenticationContext) {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
authenticationContext.getAuthentication();
RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class);
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

@@ -0,0 +1,320 @@
/*
* 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.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.Version;
import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* An {@link Authentication} implementation for the OAuth 2.0 Authorization Request (and Consent)
* used in the Authorization Code Grant.
*
* @author Joe Grandja
* @since 0.1.2
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
*/
public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private String authorizationUri;
private String clientId;
private Authentication principal;
private String redirectUri;
private Set<String> scopes;
private String state;
private Map<String, Object> additionalParameters;
private boolean consentRequired;
private boolean consent;
private OAuth2AuthorizationCode authorizationCode;
private OAuth2AuthorizationCodeRequestAuthenticationToken() {
super(Collections.emptyList());
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return "";
}
/**
* Returns the authorization URI.
*
* @return the authorization URI
*/
public String getAuthorizationUri() {
return this.authorizationUri;
}
/**
* Returns the client identifier.
*
* @return the client identifier
*/
public String getClientId() {
return this.clientId;
}
/**
* Returns the redirect uri.
*
* @return the redirect uri
*/
@Nullable
public String getRedirectUri() {
return this.redirectUri;
}
/**
* Returns the requested (or authorized) scope(s).
*
* @return the requested (or authorized) scope(s), or an empty {@code Set} if not available
*/
public Set<String> getScopes() {
return this.scopes;
}
/**
* Returns the state.
*
* @return the state
*/
@Nullable
public String getState() {
return this.state;
}
/**
* Returns the additional parameters.
*
* @return the additional parameters
*/
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
/**
* Returns {@code true} if authorization consent is required, {@code false} otherwise.
*
* @return {@code true} if authorization consent is required, {@code false} otherwise
*/
public boolean isConsentRequired() {
return this.consentRequired;
}
/**
* Returns {@code true} if this {@code Authentication} represents an authorization consent request,
* {@code false} otherwise.
*
* @return {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
*/
public boolean isConsent() {
return this.consent;
}
/**
* Returns the {@link OAuth2AuthorizationCode}.
*
* @return the {@link OAuth2AuthorizationCode}
*/
@Nullable
public OAuth2AuthorizationCode getAuthorizationCode() {
return this.authorizationCode;
}
/**
* Returns a new {@link Builder}, initialized with the given client identifier
* and {@code Principal} (Resource Owner).
*
* @param clientId the client identifier
* @param principal the {@code Principal} (Resource Owner)
* @return the {@link Builder}
*/
public static Builder with(@NonNull String clientId, @NonNull Authentication principal) {
Assert.hasText(clientId, "clientId cannot be empty");
Assert.notNull(principal, "principal cannot be null");
return new Builder(clientId, principal);
}
/**
* A builder for {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
*/
public static final class Builder implements Serializable {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private String authorizationUri;
private String clientId;
private Authentication principal;
private String redirectUri;
private Set<String> scopes;
private String state;
private Map<String, Object> additionalParameters;
private boolean consentRequired;
private boolean consent;
private OAuth2AuthorizationCode authorizationCode;
private Builder(String clientId, Authentication principal) {
this.clientId = clientId;
this.principal = principal;
}
/**
* Sets the authorization URI.
*
* @param authorizationUri the authorization URI
* @return the {@link Builder}
*/
public Builder authorizationUri(String authorizationUri) {
this.authorizationUri = authorizationUri;
return this;
}
/**
* Sets the redirect uri.
*
* @param redirectUri the redirect uri
* @return the {@link Builder}
*/
public Builder redirectUri(String redirectUri) {
this.redirectUri = redirectUri;
return this;
}
/**
* Sets the requested (or authorized) scope(s).
*
* @param scopes the requested (or authorized) scope(s)
* @return the {@link Builder}
*/
public Builder scopes(Set<String> scopes) {
if (scopes != null) {
this.scopes = new HashSet<>(scopes);
}
return this;
}
/**
* Sets the state.
*
* @param state the state
* @return the {@link Builder}
*/
public Builder state(String state) {
this.state = state;
return this;
}
/**
* Sets the additional parameters.
*
* @param additionalParameters the additional parameters
* @return the {@link Builder}
*/
public Builder additionalParameters(Map<String, Object> additionalParameters) {
if (additionalParameters != null) {
this.additionalParameters = new HashMap<>(additionalParameters);
}
return this;
}
/**
* Set to {@code true} if authorization consent is required, {@code false} otherwise.
*
* @param consentRequired {@code true} if authorization consent is required, {@code false} otherwise
* @return the {@link Builder}
*/
public Builder consentRequired(boolean consentRequired) {
this.consentRequired = consentRequired;
return this;
}
/**
* Set to {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise.
*
* @param consent {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
* @return the {@link Builder}
*/
public Builder consent(boolean consent) {
this.consent = consent;
return this;
}
/**
* Sets the {@link OAuth2AuthorizationCode}.
*
* @param authorizationCode the {@link OAuth2AuthorizationCode}
* @return the {@link Builder}
*/
public Builder authorizationCode(OAuth2AuthorizationCode authorizationCode) {
this.authorizationCode = authorizationCode;
return this;
}
/**
* Builds a new {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
*
* @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
*/
public OAuth2AuthorizationCodeRequestAuthenticationToken build() {
Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty");
if (this.consent) {
Assert.hasText(this.state, "state cannot be empty");
}
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
new OAuth2AuthorizationCodeRequestAuthenticationToken();
authentication.authorizationUri = this.authorizationUri;
authentication.clientId = this.clientId;
authentication.principal = this.principal;
authentication.redirectUri = this.redirectUri;
authentication.scopes = Collections.unmodifiableSet(
!CollectionUtils.isEmpty(this.scopes) ?
this.scopes :
Collections.emptySet());
authentication.state = this.state;
authentication.additionalParameters = Collections.unmodifiableMap(
!CollectionUtils.isEmpty(this.additionalParameters) ?
this.additionalParameters :
Collections.emptyMap());
authentication.consentRequired = this.consentRequired;
authentication.consent = this.consent;
authentication.authorizationCode = this.authorizationCode;
if (this.authorizationCode != null || this.consentRequired) {
authentication.setAuthenticated(true);
}
return authentication;
}
}
}

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

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,32 +15,18 @@
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Map;
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.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
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.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenType;
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.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
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.ProviderSettings;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* An {@link AuthenticationProvider} implementation used for authenticating an OAuth 2.0 Client.
@@ -48,18 +34,25 @@ import org.springframework.util.StringUtils;
* @author Joe Grandja
* @author Patryk Kostrzewa
* @author Daniel Garnier-Moiroux
* @author Rafal Lewczuk
* @since 0.0.1
* @see AuthenticationProvider
* @see OAuth2ClientAuthenticationToken
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @see PasswordEncoder
* @see JwtClientAssertionAuthenticationProvider
* @see ClientSecretAuthenticationProvider
* @see PublicClientAuthenticationProvider
* @deprecated This implementation is decomposed into {@link JwtClientAssertionAuthenticationProvider},
* {@link ClientSecretAuthenticationProvider} and {@link PublicClientAuthenticationProvider}.
*/
public class OAuth2ClientAuthenticationProvider implements AuthenticationProvider {
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
private PasswordEncoder passwordEncoder;
@Deprecated
public final class OAuth2ClientAuthenticationProvider implements AuthenticationProvider {
private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD =
new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
private final JwtClientAssertionAuthenticationProvider jwtClientAssertionAuthenticationProvider;
private final ClientSecretAuthenticationProvider clientSecretAuthenticationProvider;
private final PublicClientAuthenticationProvider publicClientAuthenticationProvider;
/**
* Constructs an {@code OAuth2ClientAuthenticationProvider} using the provided parameters.
@@ -71,9 +64,12 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
OAuth2AuthorizationService authorizationService) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.registeredClientRepository = registeredClientRepository;
this.authorizationService = authorizationService;
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
this.jwtClientAssertionAuthenticationProvider = new JwtClientAssertionAuthenticationProvider(
registeredClientRepository, authorizationService);
this.clientSecretAuthenticationProvider = new ClientSecretAuthenticationProvider(
registeredClientRepository, authorizationService);
this.publicClientAuthenticationProvider = new PublicClientAuthenticationProvider(
registeredClientRepository, authorizationService);
}
/**
@@ -84,9 +80,12 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
*
* @param passwordEncoder the {@link PasswordEncoder} used to validate the client secret
*/
public final void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.clientSecretAuthenticationProvider.setPasswordEncoder(passwordEncoder);
}
@Autowired
protected void setProviderSettings(ProviderSettings providerSettings) {
}
@Override
@@ -94,34 +93,14 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
OAuth2ClientAuthenticationToken clientAuthentication =
(OAuth2ClientAuthenticationToken) authentication;
String clientId = clientAuthentication.getPrincipal().toString();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
throwInvalidClient();
if (JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod())) {
return this.jwtClientAssertionAuthenticationProvider.authenticate(authentication);
} else if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getClientAuthenticationMethod()) ||
ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getClientAuthenticationMethod())) {
return this.clientSecretAuthenticationProvider.authenticate(authentication);
} else {
return this.publicClientAuthenticationProvider.authenticate(authentication);
}
if (!registeredClient.getClientAuthenticationMethods().contains(
clientAuthentication.getClientAuthenticationMethod())) {
throwInvalidClient();
}
boolean authenticatedCredentials = false;
if (clientAuthentication.getCredentials() != null) {
String clientSecret = clientAuthentication.getCredentials().toString();
if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {
throwInvalidClient();
}
authenticatedCredentials = true;
}
authenticatedCredentials = authenticatedCredentials ||
authenticatePkceIfAvailable(clientAuthentication, registeredClient);
if (!authenticatedCredentials) {
throwInvalidClient();
}
return new OAuth2ClientAuthenticationToken(registeredClient);
}
@Override
@@ -129,67 +108,4 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
}
private boolean authenticatePkceIfAvailable(OAuth2ClientAuthenticationToken clientAuthentication,
RegisteredClient registeredClient) {
Map<String, Object> parameters = clientAuthentication.getAdditionalParameters();
if (CollectionUtils.isEmpty(parameters) || !authorizationCodeGrant(parameters)) {
return false;
}
OAuth2Authorization authorization = this.authorizationService.findByToken(
(String) parameters.get(OAuth2ParameterNames.CODE),
AUTHORIZATION_CODE_TOKEN_TYPE);
if (authorization == null) {
throwInvalidClient();
}
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
OAuth2AuthorizationRequest.class.getName());
String codeChallenge = (String) authorizationRequest.getAdditionalParameters()
.get(PkceParameterNames.CODE_CHALLENGE);
if (!StringUtils.hasText(codeChallenge) &&
registeredClient.getClientSettings().requireProofKey()) {
throwInvalidClient();
}
String codeChallengeMethod = (String) authorizationRequest.getAdditionalParameters()
.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
String codeVerifier = (String) parameters.get(PkceParameterNames.CODE_VERIFIER);
if (!codeVerifierValid(codeVerifier, codeChallenge, codeChallengeMethod)) {
throwInvalidClient();
}
return true;
}
private static boolean authorizationCodeGrant(Map<String, Object> parameters) {
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
parameters.get(OAuth2ParameterNames.GRANT_TYPE)) &&
parameters.get(OAuth2ParameterNames.CODE) != null;
}
private static boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
if (!StringUtils.hasText(codeVerifier)) {
return false;
} else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) {
return codeVerifier.equals(codeChallenge);
} else if ("S256".equals(codeChallengeMethod)) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
return encodedVerifier.equals(codeChallenge);
} catch (NoSuchAlgorithmException ex) {
// It is unlikely that SHA-256 is not available on the server. If it is not available,
// there will likely be bigger issues as well. We default to SERVER_ERROR.
}
}
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR));
}
private static void throwInvalidClient() {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,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.
*
@@ -35,98 +36,97 @@ import java.util.Map;
* @since 0.0.1
* @see AbstractAuthenticationToken
* @see RegisteredClient
* @see OAuth2ClientAuthenticationProvider
* @see JwtClientAssertionAuthenticationProvider
* @see ClientSecretAuthenticationProvider
* @see PublicClientAuthenticationProvider
*/
@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

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,28 +17,34 @@ 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;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClaimAccessor;
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.OAuth2Token;
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.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
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.security.oauth2.server.authorization.context.ProviderContextHolder;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -53,40 +59,69 @@ import static org.springframework.security.oauth2.server.authorization.authentic
* @see OAuth2ClientCredentialsAuthenticationToken
* @see OAuth2AccessTokenAuthenticationToken
* @see OAuth2AuthorizationService
* @see JwtEncoder
* @see OAuth2TokenCustomizer
* @see JwtEncodingContext
* @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>
* @see OAuth2TokenGenerator
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.4">Section 4.4 Client Credentials Grant</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/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 static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private final OAuth2AuthorizationService authorizationService;
private final JwtEncoder jwtEncoder;
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
private ProviderSettings providerSettings;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
// TODO Remove after removing @Deprecated OAuth2ClientCredentialsAuthenticationProvider(OAuth2AuthorizationService, JwtEncoder)
private JwtGenerator jwtGenerator;
/**
* Constructs an {@code OAuth2ClientCredentialsAuthenticationProvider} using the provided parameters.
*
* @deprecated Use {@link #OAuth2ClientCredentialsAuthenticationProvider(OAuth2AuthorizationService, OAuth2TokenGenerator)} instead
* @param authorizationService the authorization service
* @param jwtEncoder the jwt encoder
*/
@Deprecated
public OAuth2ClientCredentialsAuthenticationProvider(OAuth2AuthorizationService authorizationService,
JwtEncoder jwtEncoder) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
this.authorizationService = authorizationService;
this.jwtEncoder = jwtEncoder;
this.jwtGenerator = new JwtGenerator(jwtEncoder);
this.tokenGenerator = new DelegatingOAuth2TokenGenerator(
this.jwtGenerator, new OAuth2AccessTokenGenerator());
}
public final void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
/**
* Constructs an {@code OAuth2ClientCredentialsAuthenticationProvider} using the provided parameters.
*
* @param authorizationService the authorization service
* @param tokenGenerator the token generator
* @since 0.2.3
*/
public OAuth2ClientCredentialsAuthenticationProvider(OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
/**
* 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}.
*
* @deprecated Use {@link JwtGenerator#setJwtCustomizer(OAuth2TokenCustomizer)} instead
* @param jwtCustomizer the {@link OAuth2TokenCustomizer} that customizes the headers and/or claims for the generated {@code Jwt}
*/
@Deprecated
public void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
this.jwtCustomizer = jwtCustomizer;
if (this.jwtGenerator != null) {
this.jwtGenerator.setJwtCustomizer(jwtCustomizer);
}
}
@Autowired(required = false)
@Deprecated
protected void setProviderSettings(ProviderSettings providerSettings) {
this.providerSettings = providerSettings;
}
@Override
@@ -99,30 +134,24 @@ 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;
JoseHeader.Builder headersBuilder = JwtUtils.headers();
JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(
registeredClient, issuer, clientPrincipal.getName(), authorizedScopes);
// @formatter:off
JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(clientPrincipal)
.providerContext(ProviderContextHolder.getProviderContext())
.authorizedScopes(authorizedScopes)
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
@@ -130,26 +159,30 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica
.build();
// @formatter:on
this.jwtCustomizer.customize(context);
JoseHeader headers = context.getHeaders().build();
JwtClaimsSet claims = context.getClaims().build();
Jwt jwtAccessToken = this.jwtEncoder.encode(headers, claims);
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(),
jwtAccessToken.getExpiresAt(), authorizedScopes);
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
// @formatter:off
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(clientPrincipal.getName())
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.token(accessToken,
(metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, jwtAccessToken.getClaims()))
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes)
.build();
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
// @formatter:on
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
} else {
authorizationBuilder.accessToken(accessToken);
}
OAuth2Authorization authorization = authorizationBuilder.build();
this.authorizationService.save(authorization);
@@ -160,4 +193,5 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica
public boolean supports(Class<?> authentication) {
return OAuth2ClientCredentialsAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,36 +16,45 @@
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.ClaimAccessor;
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.OAuth2Token;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.jwt.JoseHeader;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
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.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
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.config.TokenSettings;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
import org.springframework.util.Assert;
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
@@ -55,45 +64,90 @@ import static org.springframework.security.oauth2.server.authorization.authentic
*
* @author Alexey Nesterov
* @author Joe Grandja
* @author Anoop Garlapati
* @since 0.0.3
* @see OAuth2RefreshTokenAuthenticationToken
* @see OAuth2AccessTokenAuthenticationToken
* @see OAuth2AuthorizationService
* @see JwtEncoder
* @see OAuth2TokenCustomizer
* @see JwtEncodingContext
* @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>
* @see OAuth2TokenGenerator
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-1.5">Section 1.5 Refresh Token Grant</a>
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a>
*/
public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
private static final StringKeyGenerator TOKEN_GENERATOR = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
public final class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private final OAuth2AuthorizationService authorizationService;
private final JwtEncoder jwtEncoder;
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
private ProviderSettings providerSettings;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
// TODO Remove after removing @Deprecated OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService, JwtEncoder)
private JwtGenerator jwtGenerator;
@Deprecated
private Supplier<String> refreshTokenGenerator;
/**
* Constructs an {@code OAuth2RefreshTokenAuthenticationProvider} using the provided parameters.
*
* @deprecated Use {@link #OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService, OAuth2TokenGenerator)} instead
* @param authorizationService the authorization service
* @param jwtEncoder the jwt encoder
*/
@Deprecated
public OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService authorizationService,
JwtEncoder jwtEncoder) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
this.authorizationService = authorizationService;
this.jwtEncoder = jwtEncoder;
this.jwtGenerator = new JwtGenerator(jwtEncoder);
this.tokenGenerator = new DelegatingOAuth2TokenGenerator(this.jwtGenerator,
new OAuth2AccessTokenGenerator(), new OAuth2RefreshTokenGenerator());
}
public final void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
/**
* Constructs an {@code OAuth2RefreshTokenAuthenticationProvider} using the provided parameters.
*
* @param authorizationService the authorization service
* @param tokenGenerator the token generator
* @since 0.2.3
*/
public OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
/**
* 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}.
*
* @deprecated Use {@link JwtGenerator#setJwtCustomizer(OAuth2TokenCustomizer)} instead
* @param jwtCustomizer the {@link OAuth2TokenCustomizer} that customizes the headers and/or claims for the generated {@code Jwt}
*/
@Deprecated
public void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
this.jwtCustomizer = jwtCustomizer;
if (this.jwtGenerator != null) {
this.jwtGenerator.setJwtCustomizer(jwtCustomizer);
}
}
@Autowired(required = false)
/**
* Sets the {@code Supplier<String>} that generates the value for the {@link OAuth2RefreshToken}.
*
* @deprecated Use {@link OAuth2RefreshTokenGenerator} instead
* @param refreshTokenGenerator the {@code Supplier<String>} that generates the value for the {@link OAuth2RefreshToken}
*/
@Deprecated
public void setRefreshTokenGenerator(Supplier<String> refreshTokenGenerator) {
Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null");
this.refreshTokenGenerator = refreshTokenGenerator;
}
@Deprecated
protected void setProviderSettings(ProviderSettings providerSettings) {
this.providerSettings = providerSettings;
}
@Override
@@ -108,24 +162,23 @@ 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();
Instant refreshTokenExpiresAt = refreshToken.getToken().getExpiresAt();
if (refreshTokenExpiresAt.isBefore(Instant.now())) {
if (!refreshToken.isActive()) {
// 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
@@ -134,64 +187,95 @@ 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;
}
if (refreshToken.isInvalidated()) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
}
String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null;
JoseHeader.Builder headersBuilder = JwtUtils.headers();
JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(
registeredClient, issuer, authorization.getPrincipalName(), scopes);
// @formatter:off
JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(authorization.getAttribute(Principal.class.getName()))
.providerContext(ProviderContextHolder.getProviderContext())
.authorization(authorization)
.authorizedScopes(authorizedScopes)
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.authorizedScopes(scopes)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrant(refreshTokenAuthentication)
.build();
.authorizationGrant(refreshTokenAuthentication);
// @formatter:on
this.jwtCustomizer.customize(context);
JoseHeader headers = context.getHeaders().build();
JwtClaimsSet claims = context.getClaims().build();
Jwt jwtAccessToken = this.jwtEncoder.encode(headers, claims);
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(),
jwtAccessToken.getExpiresAt(), scopes);
TokenSettings tokenSettings = registeredClient.getTokenSettings();
OAuth2RefreshToken currentRefreshToken = refreshToken.getToken();
if (!tokenSettings.reuseRefreshTokens()) {
currentRefreshToken = generateRefreshToken(tokenSettings.refreshTokenTimeToLive());
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) -> {
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false);
});
} else {
authorizationBuilder.accessToken(accessToken);
}
// @formatter:off
authorization = OAuth2Authorization.from(authorization)
.token(accessToken,
(metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, jwtAccessToken.getClaims()))
.refreshToken(currentRefreshToken)
.build();
// @formatter:on
// ----- Refresh token -----
OAuth2RefreshToken currentRefreshToken = refreshToken.getToken();
if (!registeredClient.getTokenSettings().isReuseRefreshTokens()) {
if (this.refreshTokenGenerator != null) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getRefreshTokenTimeToLive());
currentRefreshToken = new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt);
} else {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
currentRefreshToken = (OAuth2RefreshToken) generatedRefreshToken;
}
authorizationBuilder.refreshToken(currentRefreshToken);
}
// ----- ID token -----
OidcIdToken idToken;
if (authorizedScopes.contains(OidcScopes.OPENID)) {
tokenContext = tokenContextBuilder.tokenType(ID_TOKEN_TOKEN_TYPE).build();
OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedIdToken instanceof Jwt)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the ID token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
authorizationBuilder.token(idToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
} else {
idToken = null;
}
authorization = authorizationBuilder.build();
this.authorizationService.save(authorization);
Map<String, Object> additionalParameters = Collections.emptyMap();
if (idToken != null) {
additionalParameters = new HashMap<>();
additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
}
return new OAuth2AccessTokenAuthenticationToken(
registeredClient, clientPrincipal, accessToken, currentRefreshToken);
registeredClient, clientPrincipal, accessToken, currentRefreshToken, additionalParameters);
}
@Override
@@ -199,9 +283,4 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);
}
static OAuth2RefreshToken generateRefreshToken(Duration tokenTimeToLive) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(tokenTimeToLive);
return new OAuth2RefreshToken2(TOKEN_GENERATOR.generateKey(), issuedAt, expiresAt);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,24 +15,26 @@
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.time.Instant;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.security.authentication.AuthenticationProvider;
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.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospection;
import org.springframework.security.oauth2.jwt.JwtClaimAccessor;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
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.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
@@ -47,7 +49,10 @@ 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 static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
private static final TypeDescriptor LIST_STRING_TYPE_DESCRIPTOR =
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class));
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
@@ -102,8 +107,15 @@ public class OAuth2TokenIntrospectionAuthenticationProvider implements Authentic
private static OAuth2TokenIntrospection withActiveTokenClaims(
OAuth2Authorization.Token<AbstractOAuth2Token> authorizedToken, RegisteredClient authorizedClient) {
OAuth2TokenIntrospection.Builder tokenClaims = OAuth2TokenIntrospection.builder(true)
.clientId(authorizedClient.getClientId());
OAuth2TokenIntrospection.Builder tokenClaims;
if (!CollectionUtils.isEmpty(authorizedToken.getClaims())) {
Map<String, Object> claims = convertClaimsIfNecessary(authorizedToken.getClaims());
tokenClaims = OAuth2TokenIntrospection.withClaims(claims).active(true);
} else {
tokenClaims = OAuth2TokenIntrospection.builder(true);
}
tokenClaims.clientId(authorizedClient.getClientId());
// TODO Set "username"
@@ -117,31 +129,43 @@ public class OAuth2TokenIntrospectionAuthenticationProvider implements Authentic
if (OAuth2AccessToken.class.isAssignableFrom(token.getClass())) {
OAuth2AccessToken accessToken = (OAuth2AccessToken) token;
tokenClaims.scopes(scopes -> scopes.addAll(accessToken.getScopes()));
tokenClaims.tokenType(accessToken.getTokenType().getValue());
Map<String, Object> claims = authorizedToken.getClaims();
if (!CollectionUtils.isEmpty(claims)) {
// Assuming JWT as it's the only (currently) supported access token format
JwtClaimAccessor jwtClaims = () -> claims;
Instant notBefore = jwtClaims.getNotBefore();
if (notBefore != null) {
tokenClaims.notBefore(notBefore);
}
tokenClaims.subject(jwtClaims.getSubject());
List<String> audience = jwtClaims.getAudience();
if (!CollectionUtils.isEmpty(audience)) {
tokenClaims.audiences(audiences -> audiences.addAll(audience));
}
tokenClaims.issuer(jwtClaims.getIssuer().toExternalForm());
String jti = jwtClaims.getId();
if (StringUtils.hasText(jti)) {
tokenClaims.id(jti);
}
}
}
return tokenClaims.build();
}
private static Map<String, Object> convertClaimsIfNecessary(Map<String, Object> claims) {
Map<String, Object> convertedClaims = new HashMap<>(claims);
Object value = claims.get(OAuth2TokenIntrospectionClaimNames.ISS);
if (value != null && !(value instanceof URL)) {
URL convertedValue = ClaimConversionService.getSharedInstance()
.convert(value, URL.class);
if (convertedValue != null) {
convertedClaims.put(OAuth2TokenIntrospectionClaimNames.ISS, convertedValue);
}
}
value = claims.get(OAuth2TokenIntrospectionClaimNames.SCOPE);
if (value != null && !(value instanceof List)) {
Object convertedValue = ClaimConversionService.getSharedInstance()
.convert(value, OBJECT_TYPE_DESCRIPTOR, LIST_STRING_TYPE_DESCRIPTOR);
if (convertedValue != null) {
convertedClaims.put(OAuth2TokenIntrospectionClaimNames.SCOPE, convertedValue);
}
}
value = claims.get(OAuth2TokenIntrospectionClaimNames.AUD);
if (value != null && !(value instanceof List)) {
Object convertedValue = ClaimConversionService.getSharedInstance()
.convert(value, OBJECT_TYPE_DESCRIPTOR, LIST_STRING_TYPE_DESCRIPTOR);
if (convertedValue != null) {
convertedClaims.put(OAuth2TokenIntrospectionClaimNames.AUD, convertedValue);
}
}
return convertedClaims;
}
}

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

@@ -0,0 +1,103 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.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.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} implementation used for OAuth 2.0 Public Client Authentication,
* which authenticates the {@link PkceParameterNames#CODE_VERIFIER code_verifier} parameter.
*
* @author Joe Grandja
* @since 0.2.3
* @see AuthenticationProvider
* @see OAuth2ClientAuthenticationToken
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
*/
public final class PublicClientAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
private final RegisteredClientRepository registeredClientRepository;
private final CodeVerifierAuthenticator codeVerifierAuthenticator;
/**
* Constructs a {@code PublicClientAuthenticationProvider} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
*/
public PublicClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.registeredClientRepository = registeredClientRepository;
this.codeVerifierAuthenticator = new CodeVerifierAuthenticator(authorizationService);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2ClientAuthenticationToken clientAuthentication =
(OAuth2ClientAuthenticationToken) authentication;
if (!ClientAuthenticationMethod.NONE.equals(clientAuthentication.getClientAuthenticationMethod())) {
return null;
}
String clientId = clientAuthentication.getPrincipal().toString();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
}
if (!registeredClient.getClientAuthenticationMethods().contains(
clientAuthentication.getClientAuthenticationMethod())) {
throwInvalidClient("authentication_method");
}
// Validate the "code_verifier" parameter for the public client
this.codeVerifierAuthenticator.authenticateRequired(clientAuthentication, registeredClient);
return new OAuth2ClientAuthenticationToken(registeredClient,
clientAuthentication.getClientAuthenticationMethod(), null);
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
}
private static void throwInvalidClient(String parameterName) {
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_CLIENT,
"Client authentication failed: " + parameterName,
ERROR_URI
);
throw new OAuth2AuthenticationException(error);
}
}

View File

@@ -0,0 +1,367 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.client;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
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.core.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ConfigurationSettingNames;
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;
/**
* A JDBC implementation of a {@link RegisteredClientRepository} that uses a
* {@link JdbcOperations} for {@link RegisteredClient} persistence.
*
* <p>
* <b>NOTE:</b> This {@code RegisteredClientRepository} depends on the table definition described in
* "classpath:org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql" and
* therefore MUST be defined in the database schema.
*
* @author Rafal Lewczuk
* @author Joe Grandja
* @author Ovidiu Popa
* @since 0.1.2
* @see RegisteredClientRepository
* @see RegisteredClient
* @see JdbcOperations
* @see RowMapper
*/
public class JdbcRegisteredClientRepository implements RegisteredClientRepository {
// @formatter:off
private static final String COLUMN_NAMES = "id, "
+ "client_id, "
+ "client_id_issued_at, "
+ "client_secret, "
+ "client_secret_expires_at, "
+ "client_name, "
+ "client_authentication_methods, "
+ "authorization_grant_types, "
+ "redirect_uris, "
+ "scopes, "
+ "client_settings,"
+ "token_settings";
// @formatter:on
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
private static final String INSERT_REGISTERED_CLIENT_SQL = "INSERT INTO " + TABLE_NAME
+ "(" + 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;
/**
* Constructs a {@code JdbcRegisteredClientRepository} using the provided parameters.
*
* @param jdbcOperations the JDBC operations
*/
public JdbcRegisteredClientRepository(JdbcOperations jdbcOperations) {
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
this.jdbcOperations = jdbcOperations;
this.registeredClientRowMapper = new RegisteredClientRowMapper();
this.registeredClientParametersMapper = new RegisteredClientParametersMapper();
}
@Override
public void save(RegisteredClient registeredClient) {
Assert.notNull(registeredClient, "registeredClient cannot be null");
RegisteredClient existingRegisteredClient = findBy(PK_FILTER,
registeredClient.getId());
if (existingRegisteredClient != null) {
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);
}
@Override
public RegisteredClient findById(String id) {
Assert.hasText(id, "id cannot be empty");
return findBy("id = ?", id);
}
@Override
public RegisteredClient findByClientId(String clientId) {
Assert.hasText(clientId, "clientId cannot be empty");
return findBy("client_id = ?", clientId);
}
private RegisteredClient findBy(String filter, Object... args) {
List<RegisteredClient> result = this.jdbcOperations.query(
LOAD_REGISTERED_CLIENT_SQL + filter, this.registeredClientRowMapper, args);
return !result.isEmpty() ? result.get(0) : null;
}
/**
* Sets the {@link RowMapper} used for mapping the current row in {@code java.sql.ResultSet} to {@link RegisteredClient}.
* The default is {@link RegisteredClientRowMapper}.
*
* @param registeredClientRowMapper the {@link RowMapper} used for mapping the current row in {@code ResultSet} to {@link RegisteredClient}
*/
public final void setRegisteredClientRowMapper(RowMapper<RegisteredClient> registeredClientRowMapper) {
Assert.notNull(registeredClientRowMapper, "registeredClientRowMapper cannot be null");
this.registeredClientRowMapper = registeredClientRowMapper;
}
/**
* Sets the {@code Function} used for mapping {@link RegisteredClient} to a {@code List} of {@link SqlParameterValue}.
* The default is {@link RegisteredClientParametersMapper}.
*
* @param registeredClientParametersMapper the {@code Function} used for mapping {@link RegisteredClient} to a {@code List} of {@link SqlParameterValue}
*/
public final void setRegisteredClientParametersMapper(Function<RegisteredClient, List<SqlParameterValue>> registeredClientParametersMapper) {
Assert.notNull(registeredClientParametersMapper, "registeredClientParametersMapper cannot be null");
this.registeredClientParametersMapper = registeredClientParametersMapper;
}
protected final JdbcOperations getJdbcOperations() {
return this.jdbcOperations;
}
protected final RowMapper<RegisteredClient> getRegisteredClientRowMapper() {
return this.registeredClientRowMapper;
}
protected final Function<RegisteredClient, List<SqlParameterValue>> getRegisteredClientParametersMapper() {
return this.registeredClientParametersMapper;
}
/**
* The default {@link RowMapper} that maps the current row in
* {@code java.sql.ResultSet} to {@link RegisteredClient}.
*/
public static class RegisteredClientRowMapper implements RowMapper<RegisteredClient> {
private ObjectMapper objectMapper = new ObjectMapper();
public RegisteredClientRowMapper() {
ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader();
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
this.objectMapper.registerModules(securityModules);
this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
}
@Override
public RegisteredClient mapRow(ResultSet rs, int rowNum) throws SQLException {
Timestamp clientIdIssuedAt = rs.getTimestamp("client_id_issued_at");
Timestamp clientSecretExpiresAt = rs.getTimestamp("client_secret_expires_at");
Set<String> clientAuthenticationMethods = StringUtils.commaDelimitedListToSet(rs.getString("client_authentication_methods"));
Set<String> authorizationGrantTypes = StringUtils.commaDelimitedListToSet(rs.getString("authorization_grant_types"));
Set<String> redirectUris = StringUtils.commaDelimitedListToSet(rs.getString("redirect_uris"));
Set<String> clientScopes = StringUtils.commaDelimitedListToSet(rs.getString("scopes"));
// @formatter:off
RegisteredClient.Builder builder = RegisteredClient.withId(rs.getString("id"))
.clientId(rs.getString("client_id"))
.clientIdIssuedAt(clientIdIssuedAt != null ? clientIdIssuedAt.toInstant() : null)
.clientSecret(rs.getString("client_secret"))
.clientSecretExpiresAt(clientSecretExpiresAt != null ? clientSecretExpiresAt.toInstant() : null)
.clientName(rs.getString("client_name"))
.clientAuthenticationMethods((authenticationMethods) ->
clientAuthenticationMethods.forEach(authenticationMethod ->
authenticationMethods.add(resolveClientAuthenticationMethod(authenticationMethod))))
.authorizationGrantTypes((grantTypes) ->
authorizationGrantTypes.forEach(grantType ->
grantTypes.add(resolveAuthorizationGrantType(grantType))))
.redirectUris((uris) -> uris.addAll(redirectUris))
.scopes((scopes) -> scopes.addAll(clientScopes));
// @formatter:on
Map<String, Object> clientSettingsMap = parseMap(rs.getString("client_settings"));
builder.clientSettings(ClientSettings.withSettings(clientSettingsMap).build());
Map<String, Object> tokenSettingsMap = parseMap(rs.getString("token_settings"));
TokenSettings.Builder tokenSettingsBuilder = TokenSettings.withSettings(tokenSettingsMap);
if (!tokenSettingsMap.containsKey(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT)) {
tokenSettingsBuilder.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED);
}
builder.tokenSettings(tokenSettingsBuilder.build());
return builder.build();
}
public final void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "objectMapper cannot be null");
this.objectMapper = objectMapper;
}
protected final ObjectMapper getObjectMapper() {
return this.objectMapper;
}
private Map<String, Object> parseMap(String data) {
try {
return this.objectMapper.readValue(data, new TypeReference<Map<String, Object>>() {});
} catch (Exception ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
}
private static AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) {
if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {
return AuthorizationGrantType.AUTHORIZATION_CODE;
} else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) {
return AuthorizationGrantType.CLIENT_CREDENTIALS;
} else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(authorizationGrantType)) {
return AuthorizationGrantType.REFRESH_TOKEN;
}
return new AuthorizationGrantType(authorizationGrantType); // Custom authorization grant type
}
private static ClientAuthenticationMethod resolveClientAuthenticationMethod(String clientAuthenticationMethod) {
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equals(clientAuthenticationMethod)) {
return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
} else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientAuthenticationMethod)) {
return ClientAuthenticationMethod.CLIENT_SECRET_POST;
} else if (ClientAuthenticationMethod.NONE.getValue().equals(clientAuthenticationMethod)) {
return ClientAuthenticationMethod.NONE;
}
return new ClientAuthenticationMethod(clientAuthenticationMethod); // Custom client authentication method
}
}
/**
* The default {@code Function} that maps {@link RegisteredClient} to a
* {@code List} of {@link SqlParameterValue}.
*/
public static class RegisteredClientParametersMapper implements Function<RegisteredClient, List<SqlParameterValue>> {
private ObjectMapper objectMapper = new ObjectMapper();
public RegisteredClientParametersMapper() {
ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader();
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
this.objectMapper.registerModules(securityModules);
this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
}
@Override
public List<SqlParameterValue> apply(RegisteredClient registeredClient) {
Timestamp clientIdIssuedAt = registeredClient.getClientIdIssuedAt() != null ?
Timestamp.from(registeredClient.getClientIdIssuedAt()) : Timestamp.from(Instant.now());
Timestamp clientSecretExpiresAt = registeredClient.getClientSecretExpiresAt() != null ?
Timestamp.from(registeredClient.getClientSecretExpiresAt()) : null;
List<String> clientAuthenticationMethods = new ArrayList<>(registeredClient.getClientAuthenticationMethods().size());
registeredClient.getClientAuthenticationMethods().forEach(clientAuthenticationMethod ->
clientAuthenticationMethods.add(clientAuthenticationMethod.getValue()));
List<String> authorizationGrantTypes = new ArrayList<>(registeredClient.getAuthorizationGrantTypes().size());
registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType ->
authorizationGrantTypes.add(authorizationGrantType.getValue()));
return Arrays.asList(
new SqlParameterValue(Types.VARCHAR, registeredClient.getId()),
new SqlParameterValue(Types.VARCHAR, registeredClient.getClientId()),
new SqlParameterValue(Types.TIMESTAMP, clientIdIssuedAt),
new SqlParameterValue(Types.VARCHAR, registeredClient.getClientSecret()),
new SqlParameterValue(Types.TIMESTAMP, clientSecretExpiresAt),
new SqlParameterValue(Types.VARCHAR, registeredClient.getClientName()),
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(clientAuthenticationMethods)),
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().getSettings())),
new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getTokenSettings().getSettings())));
}
public final void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "objectMapper cannot be null");
this.objectMapper = objectMapper;
}
/**
* @deprecated See javadoc {@link RegisteredClientRepository#save(RegisteredClient)}
*/
@Deprecated
public final void setPasswordEncoder(PasswordEncoder passwordEncoder) {
}
protected final ObjectMapper getObjectMapper() {
return this.objectMapper;
}
private String writeMap(Map<String, Object> data) {
try {
return this.objectMapper.writeValueAsString(data);
} catch (Exception ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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 +
'}';
}
@@ -249,38 +250,38 @@ public class RegisteredClient implements Serializable {
private String clientSecret;
private Instant clientSecretExpiresAt;
private String clientName;
private Set<ClientAuthenticationMethod> clientAuthenticationMethods = new HashSet<>();
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 final Set<ClientAuthenticationMethod> clientAuthenticationMethods = new HashSet<>();
private final Set<AuthorizationGrantType> authorizationGrantTypes = new HashSet<>();
private final Set<String> redirectUris = new HashSet<>();
private final Set<String> scopes = new HashSet<>();
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,13 +481,33 @@ 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);
}
if (this.clientSettings == null) {
ClientSettings.Builder builder = ClientSettings.builder();
if (isPublicClientType()) {
// @formatter:off
builder
.requireProofKey(true)
.requireAuthorizationConsent(true);
// @formatter:on
}
this.clientSettings = builder.build();
}
if (this.tokenSettings == null) {
this.tokenSettings = TokenSettings.builder().build();
}
validateScopes();
validateRedirectUris();
return create();
}
private boolean isPublicClientType() {
return this.authorizationGrantTypes.contains(AuthorizationGrantType.AUTHORIZATION_CODE) &&
this.clientAuthenticationMethods.size() == 1 &&
this.clientAuthenticationMethods.contains(ClientAuthenticationMethod.NONE);
}
private RegisteredClient create() {
RegisteredClient registeredClient = new RegisteredClient();
@@ -506,8 +525,8 @@ 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;
registeredClient.tokenSettings = this.tokenSettings;
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,24 @@
*/
package org.springframework.security.oauth2.server.authorization.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
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 +42,130 @@ 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}).
* Returns the {@code URL} for the Client's JSON Web Key Set.
*
* @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 {@code URL} for the Client's JSON Web Key Set
* @since 0.2.2
*/
public ClientSettings requireUserConsent(boolean requireUserConsent) {
setting(REQUIRE_USER_CONSENT, requireUserConsent);
return this;
public String getJwkSetUrl() {
return getSetting(ConfigurationSettingNames.Client.JWK_SET_URL);
}
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;
/**
* Returns the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate
* the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
* {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods.
*
* @return the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate the Client at the Token Endpoint
* @since 0.2.2
*/
public JwsAlgorithm getTokenEndpointAuthenticationSigningAlgorithm() {
return getSetting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM);
}
/**
* Constructs a new {@link Builder} with the default settings.
*
* @return the {@link Builder}
*/
public static Builder builder() {
return new Builder()
.requireProofKey(false)
.requireAuthorizationConsent(false);
}
/**
* 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);
}
/**
* Sets the {@code URL} for the Client's JSON Web Key Set.
*
* @param jwkSetUrl the {@code URL} for the Client's JSON Web Key Set
* @return the {@link Builder} for further configuration
* @since 0.2.2
*/
public Builder jwkSetUrl(String jwkSetUrl) {
return setting(ConfigurationSettingNames.Client.JWK_SET_URL, jwkSetUrl);
}
/**
* Sets the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate
* the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
* {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods.
* @param authenticationSigningAlgorithm the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT}
* used to authenticate the Client at the Token Endpoint
* @return the {@link Builder} for further configuration
* @since 0.2.2
*/
public Builder tokenEndpointAuthenticationSigningAlgorithm(JwsAlgorithm authenticationSigningAlgorithm) {
return setting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM, authenticationSigningAlgorithm);
}
/**
* Builds the {@link ClientSettings}.
*
* @return the {@link ClientSettings}
*/
@Override
public ClientSettings build() {
return new ClientSettings(getSettings());
}
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.config;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2TokenFormat;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
/**
* 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");
/**
* Set the {@code URL} for the Client's JSON Web Key Set.
* @since 0.2.2
*/
public static final String JWK_SET_URL = CLIENT_SETTINGS_NAMESPACE.concat("jwk-set-url");
/**
* Set the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT}
* used to authenticate the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
* {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods.
* @since 0.2.2
*/
public static final String TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM = CLIENT_SETTINGS_NAMESPACE.concat("token-endpoint-authentication-signing-algorithm");
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 the {@link OAuth2TokenFormat token format} for an access token.
* @since 0.2.3
*/
public static final String ACCESS_TOKEN_FORMAT = TOKEN_SETTINGS_NAMESPACE.concat("access-token-format");
/**
* 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

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 the original author or authors.
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,9 +16,9 @@
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.OAuth2TokenFormat;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.util.Assert;
@@ -28,28 +28,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 +42,27 @@ public class TokenSettings extends Settings {
*
* @return the time-to-live for an access token
*/
public Duration accessTokenTimeToLive() {
return setting(ACCESS_TOKEN_TIME_TO_LIVE);
public Duration getAccessTokenTimeToLive() {
return getSetting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE);
}
/**
* Set the time-to-live for an access token. Must be greater than {@code Duration.ZERO}.
* Returns the token format for an access token.
* The default is {@link OAuth2TokenFormat#SELF_CONTAINED}.
*
* @param accessTokenTimeToLive the time-to-live for an access token
* @return the {@link TokenSettings}
* @return the token format for an access token
* @since 0.2.3
*/
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 OAuth2TokenFormat getAccessTokenFormat() {
return getSetting(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT);
}
/**
* 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 +70,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 +80,112 @@ 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))
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.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 the token format for an access token.
*
* @param accessTokenFormat the token format for an access token
* @return the {@link Builder} for further configuration
* @since 0.2.3
*/
public Builder accessTokenFormat(OAuth2TokenFormat accessTokenFormat) {
Assert.notNull(accessTokenFormat, "accessTokenFormat cannot be null");
return setting(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT, accessTokenFormat);
}
/**
* 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

@@ -0,0 +1,70 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.context;
import java.util.function.Supplier;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.util.Assert;
/**
* A context that holds information of the Provider.
*
* @author Joe Grandja
* @since 0.2.2
* @see ProviderSettings
* @see ProviderContextHolder
*/
public final class ProviderContext {
private final ProviderSettings providerSettings;
private final Supplier<String> issuerSupplier;
/**
* Constructs a {@code ProviderContext} using the provided parameters.
*
* @param providerSettings the provider settings
* @param issuerSupplier a {@code Supplier} for the {@code URL} of the Provider's issuer identifier
*/
public ProviderContext(ProviderSettings providerSettings, @Nullable Supplier<String> issuerSupplier) {
Assert.notNull(providerSettings, "providerSettings cannot be null");
this.providerSettings = providerSettings;
this.issuerSupplier = issuerSupplier;
}
/**
* Returns the {@link ProviderSettings}.
*
* @return the {@link ProviderSettings}
*/
public ProviderSettings getProviderSettings() {
return this.providerSettings;
}
/**
* Returns the {@code URL} of the Provider's issuer identifier.
* The issuer identifier is resolved from the constructor parameter {@code Supplier<String>}
* or if not provided then defaults to {@link ProviderSettings#getIssuer()}.
*
* @return the {@code URL} of the Provider's issuer identifier
*/
public String getIssuer() {
return this.issuerSupplier != null ?
this.issuerSupplier.get() :
getProviderSettings().getIssuer();
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.context;
import org.springframework.security.oauth2.server.authorization.web.ProviderContextFilter;
/**
* A holder of {@link ProviderContext} that associates it with the current thread using a {@code ThreadLocal}.
*
* @author Joe Grandja
* @since 0.2.2
* @see ProviderContext
* @see ProviderContextFilter
*/
public final class ProviderContextHolder {
private static final ThreadLocal<ProviderContext> holder = new ThreadLocal<>();
private ProviderContextHolder() {
}
/**
* Returns the {@link ProviderContext} bound to the current thread.
*
* @return the {@link ProviderContext}
*/
public static ProviderContext getProviderContext() {
return holder.get();
}
/**
* Bind the given {@link ProviderContext} to the current thread.
*
* @param providerContext the {@link ProviderContext}
*/
public static void setProviderContext(ProviderContext providerContext) {
if (providerContext == null) {
resetProviderContext();
} else {
holder.set(providerContext);
}
}
/**
* Reset the {@link ProviderContext} bound to the current thread.
*/
public static void resetProviderContext() {
holder.remove();
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.jackson2;
import java.time.Duration;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
/**
* This mixin class is used to serialize/deserialize {@link Duration}.
*
* @author Joe Grandja
* @since 0.1.2
* @see Duration
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE,
creatorVisibility = JsonAutoDetect.Visibility.NONE)
abstract class DurationMixin {
@JsonCreator
static void ofSeconds(@JsonProperty("seconds") long seconds, @JsonProperty("nano") long nanoAdjustment) {
}
@JsonGetter("seconds")
abstract long getSeconds();
@JsonGetter("nano")
abstract int getNano();
}

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,20 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.core.endpoint;
package org.springframework.security.oauth2.server.authorization.jackson2;
import java.util.HashSet;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
/**
* TODO
* This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA.
* This mixin class is used to serialize/deserialize {@link HashSet}.
*
* @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>
* @author Steve Riesenberg
* @since 0.1.2
* @see HashSet
*/
public interface OAuth2ParameterNames2 extends OAuth2ParameterNames {
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
abstract class HashSetMixin {
String TOKEN = "token";
String TOKEN_TYPE_HINT = "token_type_hint";
@JsonCreator
HashSetMixin(Set<?> set) {
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2002-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.jackson2;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* TODO
* This class is a straight copy from Spring Security.
* It should be consolidated when merging this codebase into Spring Security.
*
* Utility class for {@code JsonNode}.
*
* @author Joe Grandja
* @since 5.3
*/
abstract class JsonNodeUtils {
static final TypeReference<Set<String>> STRING_SET = new TypeReference<Set<String>>() {
};
static final TypeReference<Map<String, Object>> STRING_OBJECT_MAP = new TypeReference<Map<String, Object>>() {
};
static String findStringValue(JsonNode jsonNode, String fieldName) {
if (jsonNode == null) {
return null;
}
JsonNode value = jsonNode.findValue(fieldName);
return (value != null && value.isTextual()) ? value.asText() : null;
}
static <T> T findValue(JsonNode jsonNode, String fieldName, TypeReference<T> valueTypeReference,
ObjectMapper mapper) {
if (jsonNode == null) {
return null;
}
JsonNode value = jsonNode.findValue(fieldName);
return (value != null && value.isContainerNode()) ? mapper.convertValue(value, valueTypeReference) : null;
}
static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) {
if (jsonNode == null) {
return null;
}
JsonNode value = jsonNode.findValue(fieldName);
return (value != null && value.isObject()) ? value : null;
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.jackson2;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
/**
* This mixin class is used to serialize/deserialize {@link SignatureAlgorithm}.
*
* @author Joe Grandja
* @since 0.1.2
* @see SignatureAlgorithm
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
abstract class JwsAlgorithmMixin {
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2002-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.jackson2;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.StdConverter;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest.Builder;
/**
* TODO
* This class is a straight copy from Spring Security.
* It should be consolidated when merging this codebase into Spring Security.
*
* A {@code JsonDeserializer} for {@link OAuth2AuthorizationRequest}.
*
* @author Joe Grandja
* @since 5.3
* @see OAuth2AuthorizationRequest
* @see OAuth2AuthorizationRequestMixin
*/
final class OAuth2AuthorizationRequestDeserializer extends JsonDeserializer<OAuth2AuthorizationRequest> {
private static final StdConverter<JsonNode, AuthorizationGrantType> AUTHORIZATION_GRANT_TYPE_CONVERTER = new StdConverters.AuthorizationGrantTypeConverter();
@Override
public OAuth2AuthorizationRequest deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
JsonNode root = mapper.readTree(parser);
return deserialize(parser, mapper, root);
}
private OAuth2AuthorizationRequest deserialize(JsonParser parser, ObjectMapper mapper, JsonNode root)
throws JsonParseException {
AuthorizationGrantType authorizationGrantType = AUTHORIZATION_GRANT_TYPE_CONVERTER
.convert(JsonNodeUtils.findObjectNode(root, "authorizationGrantType"));
Builder builder = getBuilder(parser, authorizationGrantType);
builder.authorizationUri(JsonNodeUtils.findStringValue(root, "authorizationUri"));
builder.clientId(JsonNodeUtils.findStringValue(root, "clientId"));
builder.redirectUri(JsonNodeUtils.findStringValue(root, "redirectUri"));
builder.scopes(JsonNodeUtils.findValue(root, "scopes", JsonNodeUtils.STRING_SET, mapper));
builder.state(JsonNodeUtils.findStringValue(root, "state"));
builder.additionalParameters(
JsonNodeUtils.findValue(root, "additionalParameters", JsonNodeUtils.STRING_OBJECT_MAP, mapper));
builder.authorizationRequestUri(JsonNodeUtils.findStringValue(root, "authorizationRequestUri"));
builder.attributes(JsonNodeUtils.findValue(root, "attributes", JsonNodeUtils.STRING_OBJECT_MAP, mapper));
return builder.build();
}
private Builder getBuilder(JsonParser parser,
AuthorizationGrantType authorizationGrantType) throws JsonParseException {
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) {
return OAuth2AuthorizationRequest.authorizationCode();
}
if (AuthorizationGrantType.IMPLICIT.equals(authorizationGrantType)) {
return OAuth2AuthorizationRequest.implicit();
}
throw new JsonParseException(parser, "Invalid authorizationGrantType");
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2002-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.jackson2;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
/**
* TODO
* This class is a straight copy from Spring Security.
* It should be consolidated when merging this codebase into Spring Security.
*
* This mixin class is used to serialize/deserialize {@link OAuth2AuthorizationRequest}.
* It also registers a custom deserializer {@link OAuth2AuthorizationRequestDeserializer}.
*
* @author Joe Grandja
* @since 5.3
* @see OAuth2AuthorizationRequest
* @see OAuth2AuthorizationRequestDeserializer
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonDeserialize(using = OAuth2AuthorizationRequestDeserializer.class)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
abstract class OAuth2AuthorizationRequestMixin {
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.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;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.core.OAuth2TokenFormat;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
/**
* Jackson {@code Module} for {@code spring-authorization-server}, that registers the
* following mix-in annotations:
*
* <ul>
* <li>{@link UnmodifiableMapMixin}</li>
* <li>{@link HashSetMixin}</li>
* <li>{@link OAuth2AuthorizationRequestMixin}</li>
* <li>{@link DurationMixin}</li>
* <li>{@link JwsAlgorithmMixin}</li>
* <li>{@link OAuth2TokenFormatMixin}</li>
* </ul>
*
* If not already enabled, default typing will be automatically enabled as type info is
* required to properly serialize/deserialize objects. In order to use this module just
* add it to your {@code ObjectMapper} configuration.
*
* <pre>
* ObjectMapper mapper = new ObjectMapper();
* mapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
* </pre>
*
* <b>NOTE:</b> Use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get a list
* of all security modules.
*
* @author Steve Riesenberg
* @since 0.1.2
* @see SecurityJackson2Modules
* @see UnmodifiableMapMixin
* @see HashSetMixin
* @see OAuth2AuthorizationRequestMixin
* @see DurationMixin
* @see JwsAlgorithmMixin
* @see OAuth2TokenFormatMixin
*/
public class OAuth2AuthorizationServerJackson2Module extends SimpleModule {
public OAuth2AuthorizationServerJackson2Module() {
super(OAuth2AuthorizationServerJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null));
}
@Override
public void setupModule(SetupContext context) {
SecurityJackson2Modules.enableDefaultTyping(context.getOwner());
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, JwsAlgorithmMixin.class);
context.setMixInAnnotations(MacAlgorithm.class, JwsAlgorithmMixin.class);
context.setMixInAnnotations(OAuth2TokenFormat.class, OAuth2TokenFormatMixin.class);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.jackson2;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.oauth2.core.OAuth2TokenFormat;
/**
* This mixin class is used to serialize/deserialize {@link OAuth2TokenFormat}.
*
* @author Joe Grandja
* @since 0.2.3
* @see OAuth2TokenFormat
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
abstract class OAuth2TokenFormatMixin {
@JsonCreator
OAuth2TokenFormatMixin(@JsonProperty("value") String value) {
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2002-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.jackson2;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.util.StdConverter;
import org.springframework.security.oauth2.core.AuthenticationMethod;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
/**
* TODO
* This class is a straight copy from Spring Security.
* It should be consolidated when merging this codebase into Spring Security.
*
* {@code StdConverter} implementations.
*
* @author Joe Grandja
* @since 5.3
*/
abstract class StdConverters {
static final class AccessTokenTypeConverter extends StdConverter<JsonNode, OAuth2AccessToken.TokenType> {
@Override
public OAuth2AccessToken.TokenType convert(JsonNode jsonNode) {
String value = JsonNodeUtils.findStringValue(jsonNode, "value");
if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(value)) {
return OAuth2AccessToken.TokenType.BEARER;
}
return null;
}
}
static final class ClientAuthenticationMethodConverter extends StdConverter<JsonNode, ClientAuthenticationMethod> {
@Override
public ClientAuthenticationMethod convert(JsonNode jsonNode) {
String value = JsonNodeUtils.findStringValue(jsonNode, "value");
if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equalsIgnoreCase(value)
|| ClientAuthenticationMethod.BASIC.getValue().equalsIgnoreCase(value)) {
return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
}
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equalsIgnoreCase(value)
|| ClientAuthenticationMethod.POST.getValue().equalsIgnoreCase(value)) {
return ClientAuthenticationMethod.CLIENT_SECRET_POST;
}
if (ClientAuthenticationMethod.NONE.getValue().equalsIgnoreCase(value)) {
return ClientAuthenticationMethod.NONE;
}
return null;
}
}
static final class AuthorizationGrantTypeConverter extends StdConverter<JsonNode, AuthorizationGrantType> {
@Override
public AuthorizationGrantType convert(JsonNode jsonNode) {
String value = JsonNodeUtils.findStringValue(jsonNode, "value");
if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equalsIgnoreCase(value)) {
return AuthorizationGrantType.AUTHORIZATION_CODE;
}
if (AuthorizationGrantType.IMPLICIT.getValue().equalsIgnoreCase(value)) {
return AuthorizationGrantType.IMPLICIT;
}
if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equalsIgnoreCase(value)) {
return AuthorizationGrantType.CLIENT_CREDENTIALS;
}
if (AuthorizationGrantType.PASSWORD.getValue().equalsIgnoreCase(value)) {
return AuthorizationGrantType.PASSWORD;
}
return null;
}
}
static final class AuthenticationMethodConverter extends StdConverter<JsonNode, AuthenticationMethod> {
@Override
public AuthenticationMethod convert(JsonNode jsonNode) {
String value = JsonNodeUtils.findStringValue(jsonNode, "value");
if (AuthenticationMethod.HEADER.getValue().equalsIgnoreCase(value)) {
return AuthenticationMethod.HEADER;
}
if (AuthenticationMethod.FORM.getValue().equalsIgnoreCase(value)) {
return AuthenticationMethod.FORM;
}
if (AuthenticationMethod.QUERY.getValue().equalsIgnoreCase(value)) {
return AuthenticationMethod.QUERY;
}
return null;
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2002-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.jackson2;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* TODO
* This class is a straight copy from Spring Security.
* It should be consolidated when merging this codebase into Spring Security.
*
* A {@code JsonDeserializer} for {@link Collections#unmodifiableMap(Map)}.
*
* @author Joe Grandja
* @since 5.3
* @see Collections#unmodifiableMap(Map)
* @see UnmodifiableMapMixin
*/
final class UnmodifiableMapDeserializer extends JsonDeserializer<Map<?, ?>> {
@Override
public Map<?, ?> deserialize(JsonParser parser, DeserializationContext context) throws IOException {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
JsonNode mapNode = mapper.readTree(parser);
Map<String, Object> result = new LinkedHashMap<>();
if (mapNode != null && mapNode.isObject()) {
Iterable<Map.Entry<String, JsonNode>> fields = mapNode::fields;
for (Map.Entry<String, JsonNode> field : fields) {
result.put(field.getKey(), mapper.readValue(field.getValue().traverse(mapper), Object.class));
}
}
return Collections.unmodifiableMap(result);
}
}

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