1
0
mirror of synced 2026-07-08 12:20:02 +00:00

Compare commits

...

169 Commits

Author SHA1 Message Date
Eleftheria Stein 2443675a59 Release 5.2.6.RELEASE 2020-08-05 15:34:14 +02:00
Eleftheria Stein 04d6d5ae74 Update to nohttp 0.0.5.RELEASE
Closes gh-8927
2020-08-05 13:47:12 +02:00
Eleftheria Stein acbbb6941c Update to groovy 2.4.20
Closes gh-8915
2020-08-05 11:25:05 +02:00
Eleftheria Stein 4a1b96d51a Update to embedded Tomcat websocket 8.5.57
Closes gh-8914
2020-08-05 11:24:47 +02:00
Eleftheria Stein 9fbed06b6b Update blockhound to 1.0.4.RELEASE
Closes gh-8916
2020-08-05 11:24:16 +02:00
Eleftheria Stein fea90f9328 Update to PowerMock Mockito2 2.0.7
Closes gh-8917
2020-08-05 11:23:56 +02:00
Eleftheria Stein 22a1bce83c Update to Spring Data Moore-SR9
Closes gh-8918
2020-08-05 11:23:21 +02:00
Eleftheria Stein 8dfe0fa815 Update to Spring Framework 5.2.8.RELEASE
Closes gh-8919
2020-08-05 11:22:53 +02:00
Eleftheria Stein b408f074d5 Update to Reactor Dysprosium-SR10
Closes gh-8920
2020-08-05 11:22:31 +02:00
Eleftheria Stein 669fdba98c Update to Spring Boot 2.2.9.RELEASE
Closes gh-8921
2020-08-05 11:21:45 +02:00
Eleftheria Stein b4d2217d45 Update to jaxb-impl 2.3.3
Closes gh-8912
2020-08-05 11:06:50 +02:00
Eleftheria Stein b1e09eddaa Update to Jackson 2.10.5
Closes gh-8910
2020-08-05 11:04:25 +02:00
Eleftheria Stein b425b622db Update to GAE 1.9.81
Closes gh-8911
2020-08-05 10:41:59 +02:00
Eleftheria Stein 6512c04079 Upgrade to embedded Apache Tomcat 9.0.37
Closes gh-8913
2020-08-05 10:37:32 +02:00
Dávid Kováč da4bd22c6d Resolve Bearer token after subscribing to publisher
Bearer token was resolved immediately after calling method convert. In situations when malformed token was provided or authorization header and access token query param were present in request exception was thrown instead of signalling error.
After this change Bearer token is resolved on subscription and invalid states are handled by signaling error to subscriber.

Closes gh-8865
2020-08-03 11:11:09 -05:00
Josh Cummings fd669f751d Remove unused import
Issue gh-8589
2020-07-31 08:45:47 -06:00
Josh Cummings 510d1b8121 Polish to Avoid NPE
Issue gh-5648

Co-authored-by: MattyA <mat.auburn@gmail.com>
2020-07-30 17:59:31 -06:00
Josh Cummings 2f80b8a5be Additional Jwt Validation Debug Messages
Closes gh-8589

Co-authored-by: MattyA <mat.auburn@gmail.com>
2020-07-30 17:58:16 -06:00
Josh Cummings 0efdb2c92c Polish WebSecurityConfigurerAdapter JavaDoc
Issue gh-8784
2020-07-20 15:24:23 -06:00
Romil Patel 79d8b616f0 WebSecurityConfigurerAdapter JavaDoc
Closes gh-8784
2020-07-20 15:24:19 -06:00
Josh Cummings 9d8920f1b1 Polish Bearer Token Padding
Issue gh-8502
2020-07-16 12:22:45 -06:00
kothasa 6519029340 Bearer Token Padding
Closes gh-8502
2020-07-16 12:22:32 -06:00
wangsong 3dc59686fa Fix ProviderManager Javadoc typo
Closes gh-8800
2020-07-07 17:17:16 -05:00
Rob Winch dcd2137418 LoginPageGeneratingWebFilter honors context path
Closes gh-8807
2020-07-07 13:37:06 -05:00
Ellie Bahadori 1bb49bbc2e Use Github Actions workflow for PRs and remove Travis
Closes gh-8718
2020-06-30 05:05:48 -04:00
Rob Winch 5f395a3513 Better scp Retry Settings 2020-06-25 11:34:46 -05:00
Rob Winch 0356af03e6 Revert "Update to spring-build-conventions:0.0.33.RELEASE"
This reverts commit 5343325091.
2020-06-25 11:32:18 -05:00
Rob Winch 5343325091 Update to spring-build-conventions:0.0.33.RELEASE
Closes gh-8759
2020-06-25 11:30:12 -05:00
Dávid Kovács fa9898dd6d formLogin() and login() implement Mergable
This is necessary so that default requests like Spring REST Docs work.

Closes gh-7572
2020-06-22 14:58:39 -05:00
Evgeniy Cheban bff6d82dd0 DefaultWebSecurityExpressionHandler uses RoleHierarchy bean
Fixes gh-7059
2020-06-10 17:08:50 -04:00
Joe Grandja 674e2c0a8e OAuth2LoginAuthenticationWebFilter should handle OAuth2AuthorizationException
Issue gh-8609
2020-06-09 16:24:00 -04:00
Joe Grandja 11c1236261 OAuth2AuthorizationCodeGrantWebFilter should handle OAuth2AuthorizationException
Fixes gh-8609
2020-06-09 16:24:00 -04:00
Eleftheria Stein 98467755ad Update to RSocket 1.0.1
Fixes gh-8664
2020-06-08 17:18:33 -04:00
Eleftheria Stein 0e37c722e2 Revert "Temporarily ignore RSocket integration tests"
This reverts commit d5eeec0ae6.

Fixes gh-8643
2020-06-08 16:14:34 -04:00
Rob Winch c71a893e08 Add subscriberContext to PayloadSocketAcceptor delegate.accept
Closes gh-8654
2020-06-05 12:55:07 -05:00
Eleftheria Stein 69ff2ab3fc Next development version 2020-06-03 16:17:30 -04:00
Eleftheria Stein 38a731d5e4 Release 5.2.5.RELEASE 2020-06-03 16:02:37 -04:00
Eleftheria Stein d5eeec0ae6 Temporarily ignore RSocket integration tests
Issue gh-8643
2020-06-03 15:29:59 -04:00
Eleftheria Stein fd7fe482f5 Revert "Release 5.2.5.RELEASE"
This reverts commit b88e094f37.
2020-06-03 15:29:44 -04:00
Eleftheria Stein b88e094f37 Release 5.2.5.RELEASE 2020-06-03 10:59:04 -04:00
Eleftheria Stein 04453cec96 Update to groovy 2.4.19
Fixes gh-8525
2020-06-02 17:18:21 -04:00
Eleftheria Stein d9919b838d Update to okhttp 3.14.9
Fixes gh-8629
2020-06-02 17:17:56 -04:00
Eleftheria Stein 05b9591e44 Update to Jython 2.5.3
Fixes gh-8628
2020-06-02 17:17:29 -04:00
Eleftheria Stein 1b93b0c9a6 Update to mockwebserver 3.14.9
Fixes gh-8627
2020-06-02 17:17:05 -04:00
Eleftheria Stein e87783024c Update to RSocket 1.0.0
Fixes gh-8626
2020-06-02 17:15:05 -04:00
Eleftheria Stein 7e9c7534f5 Update to Spring Boot 2.2.7
Fixes gh-8630
2020-06-02 17:14:44 -04:00
Rob Winch 24fd9579c5 Delay AuthenticationPrincipalArgumentResolver Creation
Use ObjectProvider<AuthenticationPrincipalArgumentResolver> to delay its
lookup.

Closes gh-8613
2020-05-29 16:51:47 -05:00
Eleftheria Stein a6dd119266 Mock request with non-standard HTTP method in test
Fixes gh-8594
2020-05-26 11:32:27 -04:00
justmehyp 774ea6980b Remove unused field 'digester' in Md4PasswordEncoder
`private Digester digester;`  defined in Md4PasswordEncoder is never used. So remove it.

Closes gh-8553
2020-05-21 11:21:24 -05:00
Maksim Vinogradov 279ddbe223 Prevent StackOverflowError for AccessControlEntryImpl.hashCode
Getting StackOverflowError when invoke AclImpl.hashCode because of
cross-references between AclImpl and AccessControlEntryImpl

Remove from AccessControlEntryImpl.hashCode method invocation of
acl.hashCode

fixes gh-5401
2020-05-21 10:07:54 -05:00
Dávid Kovács eaaee899fc Object ID Identicy conversion to long fails on old schema
This change fixed a bug which tried to convert non-string object as string

Fixes gh-7621
2020-05-19 13:45:23 -05:00
cbornet 21c1d98f64 Create the CSRF token on the bounded elactic scheduler
The CSRF token is created with a call to UUID.randomUUID which is blocking.
This change ensures this blocking call is done on the bounded elastic scheduler which supports blocking calls.

Fixes gh-8128
2020-05-18 11:06:45 -05:00
Artyom Tarynin 9f33ce312a Update AntPathRequestMatcher.java
Fixes gh-8512
2020-05-14 10:05:52 -04:00
Dávid Kovács e382c269ef Document NoOpPasswordEncoder will not be removed
This commit adds extension to deprecation notice.

Fixes gh-8506
2020-05-13 12:56:04 -05:00
Rob Winch ce7c501f9c AbstractUserDetailsReactiveAuthenticationManager uses boundidElastic()
Some JVMs have blocking operations when accessing SecureRandom and thus
this needs to be performed in a pool that is larger than the number of
CPUs

Closes gh-7522
2020-05-12 13:24:47 -05:00
Rob Winch bd93616567 Fix non-standard HTTP method for CsrfWebFilter
Closes gh-8452
2020-05-12 13:18:01 -05:00
Eleftheria Stein f9872d3dee Next Development Version 2020-05-06 15:59:24 -04:00
Eleftheria Stein b7212bd975 Release 5.2.4.RELEASE 2020-05-06 15:16:32 -04:00
Eleftheria Stein 50da82d88d Temporarily build against Framework 5.2.x snapshot
Issue: gh-8489
2020-05-06 14:43:53 -04:00
Eleftheria Stein 3ed31400f7 Update SAML2 errors in integration tests 2020-05-06 14:16:41 -04:00
Josh Cummings 69b1bc62ff Polish OpenSamlAuthenticationProvider
- Use type-safe CriteriaSet
- Keep Assertion immutable

Closes gh-8471
2020-05-06 14:15:45 -04:00
Eleftheria Stein f43d00fac6 Update to Byte Buddy 1.9.16
Closes gh-8481
2020-05-05 17:50:07 -04:00
Eleftheria Stein 01cce49ee5 Upgrade to embedded Apache Tomcat 9.0.34
Closes gh-8469
2020-05-05 17:48:03 -04:00
Eleftheria Stein 6ccc18fde2 Update to Jackson 2.10.4
Closes gh-8466
2020-05-05 17:47:08 -04:00
Eleftheria Stein d34f7a8474 Update to org.powermock 2.0.7
Closes gh-8465
2020-05-05 17:46:12 -04:00
Eleftheria Stein ba78fae246 Update RSocket to 1.0.0-RC7
Closes gh-8468
2020-05-05 17:45:37 -04:00
Eleftheria Stein 98922b001a Update to Spring Data Moore-SR7
Closes gh-8462
2020-05-05 17:44:33 -04:00
Eleftheria Stein 19a4386e49 Update to Spring Framework 5.2.6.RELEASE
Closes gh-8463
2020-05-05 17:43:43 -04:00
Eleftheria Stein 4fa67a1d9b Update to Reactor Dysprosium-SR7
Closes gh-8464
2020-05-05 17:43:00 -04:00
Eleftheria Stein 94fd7e3a2b Update to GAE 1.9.80
Closes gh-8467
2020-05-05 17:41:43 -04:00
Eleftheria Stein e9e736d1e6 Clean up Javadoc
Fixes gh-8480
2020-05-05 17:34:14 -04:00
Dávid Kovács c399185365 Add ROLE_INFRASTRUCTURE to infrastructure beans
Closes gh-8407
2020-04-27 09:02:54 -05:00
Dávid Kovács 93a1fc104c ActiveDirectoryLdapAuthenticationProvider uses InternalAuthenticationServiceException
Closes gh-2884
2020-04-24 10:20:35 -05:00
Julian Müller 9dd68f86d3 Enables empty authorityPrefix
- docs stated that empty authorityPrefix are allowed but implementation denied to use `""`
- commit removes the `hasText`-limitation but restricts to `notNull`

Fixes gh-8421
2020-04-22 08:55:54 -05:00
Souphorn 01f904895e Fix typo with correct capitalization
Closes gh-8406
2020-04-17 12:13:32 -04:00
Josh Cummings 067cb4579e Polish OpenSamlAuthenticationProviderTests
- Add missing assertion

Issue gh-6019
2020-04-17 09:29:39 -06:00
Rob Winch 7b34b223e6 Logout defaults to use Global SecurityContextServerLogoutHandler
Closes gh-8375
2020-04-13 16:38:05 -05:00
Rob Winch 52ed597f4c Fix example in javadoc of FilterChainProxy
Closes gh-8344
2020-04-08 09:14:02 -05:00
Alan Czajkowski 564d5644c9 BCryptPasswordEncoder rawPassword cannot be null
Closes gh-8317
2020-04-07 13:46:13 -05:00
hotire f011c36ba4 Fix typo in Javadoc of ServerHttpSecurity#hasAuthority
Closes gh-8336
2020-04-06 14:20:19 -05:00
Eleftheria Stein bb654fdcdf Fix HttpSecurity Javadoc
Fixes gh-4404
2020-04-02 11:41:15 -04:00
Eleftheria Stein 30a63026f8 Next Development Version 2020-04-01 14:26:04 -04:00
Eleftheria Stein f93e5afe16 Release 5.2.3.RELEASE 2020-04-01 12:35:09 -04:00
Eleftheria Stein 7b5bdac74d Update to OpenSAML 3.4.5
Fixes gh-8278
2020-04-01 10:01:53 -04:00
Eleftheria Stein 80a16e828f Update to hibernate-entitymanager 5.4.13.Final
Fixes gh-8277
2020-04-01 10:01:20 -04:00
Eleftheria Stein ed133d4b8d Update to hibernate-core 5.2.18.Final
Fixes gh-8276
2020-04-01 10:00:41 -04:00
Eleftheria Stein 433646b551 Update blockhound to 1.0.3.RELEASE
Fixes gh-8275
2020-04-01 09:59:58 -04:00
Eleftheria Stein dd6974812f Update to unboundid-ldapsdk 4.0.14
Fixes gh-8274
2020-04-01 09:59:33 -04:00
Eleftheria Stein 97f1e368ee Update to reactive-streams 1.0.3
Fixes gh-8279
2020-04-01 09:58:19 -04:00
Eleftheria Stein 7018e7ec51 Update to Jackson 2.10.3
Fixes gh-8258
2020-04-01 09:16:29 -04:00
Eleftheria Stein 0313757846 Update to okhttp 3.14.7
Fixes gh-8259
2020-04-01 09:15:38 -04:00
Eleftheria Stein a021cd3fd5 Update to mockwebserver 3.14.7
Fixes gh-8257
2020-04-01 09:15:12 -04:00
Eleftheria Stein a57f4bb025 Update RSocket to 1.0.0-RC6
Fixes gh-8256
2020-04-01 09:14:38 -04:00
Eleftheria Stein a02cd9d9e3 Upgrade to embedded Apache Tomcat 9.0.33
Fixes gh-8254
2020-04-01 09:13:59 -04:00
Eleftheria Stein eacede6171 Update to httpclient 4.5.12
Fixes gh-8253
2020-04-01 09:12:31 -04:00
Eleftheria Stein 08b635a85a Update to org.powermock 2.0.6
Fixes gh-8255
2020-04-01 09:12:07 -04:00
Eleftheria Stein e3c3601cf6 Update to Spring Boot 2.2.6.RELEASE
Fixes gh-8252
2020-04-01 09:11:30 -04:00
Eleftheria Stein 1ca3ea3a71 Update to GAE 1.9.79
Fixes gh-8251
2020-04-01 09:10:43 -04:00
Eleftheria Stein dbdf02e864 Update to Reactor Dysprosium-SR6
Fixes gh-8250
2020-04-01 09:09:42 -04:00
Eleftheria Stein 774dd7bb13 Update to Spring Data Moore-SR6
Fixes gh-8248
2020-04-01 09:09:06 -04:00
Eleftheria Stein 6a9585dc0d Update to Spring Framework 5.2.5
Fixes gh-8249
2020-03-31 18:06:26 -04:00
Eleftheria Stein 298d40b7a5 Update tests to use absolute paths
Fixes gh-8260
2020-03-31 18:05:27 -04:00
Rob Winch 615f9a3f05 Fix HttpServlet3RequestFactory Logout Handlers
Previously there was a problem with Servlet API logout integration
when Servlet API was configured before log out.

This ensures that logout handlers is a reference to the logout handlers
vs copying the logout handlers. This ensures that the ordering does not
matter.

Closes gh-4760
2020-03-30 20:51:25 -05:00
Rob Winch 32c3353921 SpringTestContext returns ConfigurableWebApplicationContext
Closes gh-8233
2020-03-30 20:51:25 -05:00
Ruby Hartono 401597c673 Improve OAuth2LoginAuthenticationProvider
1. update OAuth2LoginAuthenticationProvider to use
OAuth2AuthorizationCodeAuthenticationProvider
2. apply fix gh-5368 for OAuth2AuthorizationCodeAuthenticationProvider
to return additionalParameters value from accessTokenResponse

Fixes gh-5633
2020-03-30 21:09:17 -04:00
Josh Cummings 258627eaee SwitchUserFilter Defaults to POST
Fixes gh-4183
2020-03-27 14:40:38 -06:00
Eleftheria Stein 01f8eb3961 Update Encryptors documentation
Fixes gh-8208
2020-03-27 10:20:04 -04:00
Martin Nemec a9a9c2c0fd OAuth2 ClientRegistrations NPE fix when userinfo missing
Fixes gh-8187
2020-03-27 06:15:25 -04:00
Josh Cummings cb7786bf97 Malformed Bearer Token Returns 401 for WebFlux
Fixes gh-7668
2020-03-26 12:59:22 -06:00
Joe Grandja 4706b16a2b oauth2Login WebFlux does not auto-redirect for XHR request
Fixes gh-8118
2020-03-26 05:09:45 -04:00
Josh Cummings 98bd1a3f60 Polish Resource Server JWT Docs
Issue gh-5935
2020-03-25 16:33:31 -06:00
Joe Grandja f06aa724bf OAuth2ErrorHttpMessageConverter handles JSON object parameters
Fixes gh-8157
2020-03-24 14:57:24 -04:00
Joe Grandja 512ad9e7e4 Document AuthorizedClientServiceOAuth2AuthorizedClientManager
Fixes gh-8152
2020-03-19 20:14:15 -04:00
Eleftheria Stein 256aba7b37 Fix rsocket test
Request route that exists; add additional error message verification

Fixes gh-8154
2020-03-19 17:36:20 -04:00
Erik van Paassen 86e25ff2ab Fix typo in Javadoc of HttpSecurity#csrf()
`HttpSecurity#csrf()` obviously returns a `CsrfConfigurer`, while the Javadoc states that it returns the `ServletApiConfigurer`.
2020-03-17 13:36:34 -06:00
Zeeshan Adnan a49a325db2 Fix exception for empty basic auth header token
fixes spring-projectsgh-7976
2020-03-16 16:06:52 -04:00
Markus Engelbrecht 75f22285c6 Fix typo 'properites' in documentation
Fixes gh-8095
2020-03-11 11:01:06 -06:00
Josh Cummings 8fa16ce63e Update to Jetty 9.4.27
Fixes gh-7507
2020-03-09 10:03:18 -06:00
Clement Stoquart 32c02fbedb Remove empty relay state from redirect url 2020-03-04 12:47:03 -07:00
AmitB 96ff3a54a9 Fix typo in AntPathRequestMatcher contructor comment 2020-03-02 07:16:07 -06:00
Josh Cummings 9092115b8a Register Authentication Provider in Init Phase
Fixes gh-8031
2020-02-28 18:43:54 -07:00
Joe Grandja 3dbfef9ef1 OAuth2AccessTokenResponseHttpMessageConverter handles JSON object parameters
Fixes gh-6463
2020-02-24 15:58:25 -05:00
Joe Grandja 8acdb82e6a OAuth2AuthorizationCodeGrantWebFilter matches on query parameters
Fixes gh-7966
2020-02-10 15:28:06 -05:00
Rafael Renan Pacheco 5ce0ce3f38 Fix var typo and code readability 2020-02-10 12:06:30 -07:00
Joe Grandja 6141132cfa Fix test gh-7963 2020-02-10 05:53:00 -05:00
Joe Grandja cc7ea4acd3 OAuth2AuthorizationCodeGrantFilter matches on query parameters
Fixes gh-7963
2020-02-10 05:24:14 -05:00
Manuel Bleichenbacher 1e4736f9b3 Prevent double-escaping of authorize URL parameters
If the authorization URL in the OAuth2 provider configuration contained query parameters with escaped characters, these characters were escaped a second time. This commit fixes it.

It is relevant to support the OIDC claims parameter (see https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter).

Fixes gh-7871
2020-02-08 16:59:01 -05:00
Stephane Maldini 0012e24c46 Don't force downcasting of RequestAttributes to ServletRequestAttributes
Fixes gh-7953
2020-02-07 20:18:50 -05:00
Joe Grandja 2dc8147106 Add release-notes-sections.yml 2020-02-05 15:18:32 -05:00
Joe Grandja 1da8e9df13 Next Development Version 2020-02-05 11:03:09 -05:00
Joe Grandja 9a2b71d931 Release 5.2.2.RELEASE 2020-02-05 10:56:00 -05:00
Josh Cummings c4ccc96655 Polish Error Messages for OpaqueTokenIntrospectors 2020-02-05 07:16:37 -07:00
Joe Grandja 6c310213a8 Update to Spring Boot 2.2.4
Fixes gh-7909
2020-02-04 15:07:16 -05:00
Joe Grandja a5b6b9a398 Update to org.slf4j 1.7.30
Fixes gh-7908
2020-02-04 15:04:46 -05:00
Joe Grandja 9e6910273c Update to org.powermock 2.0.5
Fixes gh-7907
2020-02-04 14:56:28 -05:00
Joe Grandja ea809b01a6 Update to hibernate-validator 6.1.2.Final
Fixes gh-7906
2020-02-04 14:53:08 -05:00
Joe Grandja 8054239a12 Update to hibernate-entitymanager 5.4.10.Final
Fixes gh-7905
2020-02-04 14:51:05 -05:00
Joe Grandja 46486194c2 Update to org.aspectj 1.9.5
Fixes gh-7904
2020-02-04 14:44:05 -05:00
Joe Grandja 00b08bc725 Update to httpclient 4.5.11
Fixes gh-7903
2020-02-04 14:39:27 -05:00
Joe Grandja 6e0fbfcccd Update to commons-codec 1.14
Fixes gh-7899
2020-02-04 14:31:31 -05:00
Joe Grandja 87ea083520 Update to com.squareup.okhttp3 3.14.6
Fixes gh-7898
2020-02-04 14:24:11 -05:00
Joe Grandja 9db3f51f2a Update to Jackson 2.10.2
Fixes gh-7897
2020-02-04 14:06:11 -05:00
Joe Grandja 3cc4a945c6 Update to Reactor Dysprosium SR4
Fixes gh-7896
2020-02-04 14:03:06 -05:00
Joe Grandja dbc43fb47d Update to Spring Data Moore SR3
Fixes gh-7895
2020-02-04 14:02:57 -05:00
Joe Grandja ce6a0368bd Update to Spring Framework 5.2.3
Fixes gh-7894
2020-02-04 13:38:17 -05:00
Eleftheria Stein 9dd3dfe718 Fix requiresAuthenticationMatcher not being used
The custom server requiresAuthenticationMatcher was not always picked up

Fixes: gh-7863
2020-01-27 16:56:59 +01:00
Eleftheria Stein edb6cd3729 Fix authenticationFailureHandler not being used
The custom server authenticationFailureHandler was not always picked up

Fixes: gh-7782
2020-01-27 13:52:01 +01:00
Peter Keller 2dbedf7af5 Set charset of BasicAuthenticationFilter converter
Allow BasicAuthenticationFilter to pick up the given credentials charset.

Fixes: gh-7835
2020-01-23 16:24:03 +01:00
Eleftheria Stein 630eb10704 Load LDIF file from classpath in unboundId mode
Fixes: gh-7833
2020-01-21 17:12:18 +01:00
Eleftheria Stein f4d4c08329 Fix LDIF file example in LDAP docs
Fixes: gh-7832
2020-01-20 11:32:53 +01:00
Johannes Edmeier cc956a66df Don't cache requests with Accept: text/event-stream by default.
The eventstream requests is typically not directly invoked by the browser.
And even more unfortunately the Browser-Api doesn't allow the set additional headers as `XMLHttpRequest`..
2020-01-17 10:37:34 -08:00
Rob Winch 29182abb34 Fix HttpHeaderWriterWebFilterTests
Ensure setComplete() is subscribed to
2020-01-10 08:46:47 -06:00
Filip Hanik b754a3d635 Use the custom ServerRequestCache that the user configures
on for the default authentication entry point and authentication
success handler

Fixes gh-7721

https://github.com/spring-projects/spring-security/issues/7721

Set RequestCache on the Oauth2LoginSpec default authentication success handler

import static ReflectionTestUtils.getField

Feedback incorporated per

https://github.com/spring-projects/spring-security/pull/7734#pullrequestreview-332150359
2019-12-18 08:44:27 -08:00
Eleftheria Stein 0d24e2b8cf Fix WebFlux logout disabling
Fixes: gh-7682
2019-12-13 11:53:20 +01:00
Rob Winch b00999deed Docs ServerRSocketFactoryCustomizer->ServerRSocketFactoryProcessor
The documentation incorrectly used ServerRSocketFactoryCustomizer which
was renamed to ServerRSocketFactoryProcessor. The docs now use the correct
class name

Fixes gh-7737
2019-12-12 15:30:56 -06:00
Eleftheria Stein 59ca2ddf65 Polish SAML2 principal classes
Update @since

Issue: gh-7681
2019-12-12 20:27:24 +01:00
Clement Stoquart 0782228914 fix: make Saml2Authentication serializable 2019-12-12 20:25:26 +01:00
Rob Winch 29eb8b9177 CompositeServerHttpHeadersWriter Executes Sequentially
Fixes gh-7731
2019-12-12 11:28:23 -06:00
Rob Winch bd6ff1f319 DelegatingServerAuthenticationSuccessHandler Executes Sequentially
Fixes gh-7728
2019-12-12 08:33:14 -06:00
Rob Winch 6db7b457b7 DelegatingServerLogoutHandler Executes Sequentially
Fixes gh-7723
2019-12-11 15:39:56 -06:00
Phil Clay 840d3aa986 Polish #7589
Rename OAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager to AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.

Handle empty mono returned from contextAttributesMapper.

Handle empty map returned from contextAttributesMapper.

Fix DefaultContextAttributesMapper so that it doesn't access ServerWebExchange.

Fix unit tests so that they pass.

Use StepVerifier in unit tests, rather than .subscribe().

Fixes gh-7569
2019-12-10 14:37:34 -05:00
Ankur Pathak 4c5c4f6cce Reactive Implementation of AuthorizedClientServiceOAuth2AuthorizedClientManager
ReactiveOAuth2AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager is reactive
version of AuthorizedClientServiceOAuth2AuthorizedClientManager

Fixes: gh-7569
2019-12-10 14:37:25 -05:00
Joe Grandja 148b570a98 Remove redundant validation for redirect-uri
Fixes gh-7706
2019-12-06 12:01:19 -05:00
Joe Grandja 752d5f29aa Display general error message when WebFlux oauth2Login() fails
Issue gh-5562 gh-6484
2019-12-05 20:12:38 -05:00
Joe Grandja e4aa3be4c5 WebFlux oauth2Login() redirects on failed authentication
Fixes gh-5562 gh-6484
2019-12-05 20:12:09 -05:00
Alexey Nesterov 0babe7d930 Correctly configure authorization requests repository for OAuth2 login
To use custom ServerAuthorizationRequestRepository both OAuth2AuthorizationRequestRedirectWebFilter and
OAuth2LoginAuthenticationWebFilter should use the same repo provided in the configuration. Currently the former filter is
correctly configured, but the latter always uses default, WebSession based repository. So authorization code created
before redirect to authorization endpoint will never be found to complete OAuth2 login when custom
ServerAuthorizationRequestRepository is used.

This change also makes OAuth2Client and OAuth2Login authentication converters consistent.

Fixes gh-7675
2019-11-29 13:58:27 -05:00
Joe Grandja b905cb8aaa Polish OAuth2AuthorizedClientArgumentResolver 2019-11-28 10:31:29 -05:00
Joe Grandja 19c2209a12 ServerOAuth2AuthorizedClientExchangeFilterFunction works with UnAuthenticatedServerOAuth2AuthorizedClientRepository
Fixes gh-7544
2019-11-28 10:31:18 -05:00
Joe Grandja 18f48e4a16 DefaultReactiveOAuth2AuthorizedClientManager requires non-null serverWebExchange
Issue gh-7544
2019-11-28 10:31:06 -05:00
152 changed files with 3898 additions and 1058 deletions
+22
View File
@@ -0,0 +1,22 @@
name: PR Build
on: pull_request
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: '8'
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
- name: Build with Gradle
run: ./gradlew clean build --continue
-16
View File
@@ -1,16 +0,0 @@
language: java
jdk:
- openjdk8
os:
- linux
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
script: ./gradlew build --refresh-dependencies --no-daemon --continue
Vendored
+1 -1
View File
@@ -56,7 +56,7 @@ try {
sh "git clean -dfx"
try {
withEnv(["JAVA_HOME=${ tool 'jdk8' }"]) {
sh "./gradlew clean test -PforceMavenRepositories=snapshot -PspringVersion='5.+' -PreactorVersion=Dysprosium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace"
sh "./gradlew clean test -PforceMavenRepositories=snapshot -PspringVersion='5.2.+' -PreactorVersion=Dysprosium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace"
}
} catch(Exception e) {
currentBuild.result = 'FAILED: snapshots'
-2
View File
@@ -1,7 +1,5 @@
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://travis-ci.org/spring-projects/spring-security.svg?branch=master["Build Status", link="https://travis-ci.org/spring-projects/spring-security"]
= Spring Security
Spring Security provides security services for the https://docs.spring.io[Spring IO Platform]. Spring Security 5.0 requires Spring 5.0 as
@@ -1,5 +1,5 @@
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
* Copyright 2002-2016 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.
@@ -131,10 +131,9 @@ public class AccessControlEntryImpl implements AccessControlEntry,
@Override
public int hashCode() {
int result = this.acl.hashCode();
result = 31 * result + this.permission.hashCode();
int result = this.permission.hashCode();
result = 31 * result + (this.id != null ? this.id.hashCode() : 0);
result = 31 * result + this.sid.hashCode();
result = 31 * result + (this.sid.hashCode());
result = 31 * result + (this.auditFailure ? 1 : 0);
result = 31 * result + (this.auditSuccess ? 1 : 0);
result = 31 * result + (this.granting ? 1 : 0);
@@ -118,7 +118,7 @@ class AclClassIdUtils {
*/
private Long convertToLong(Serializable identifier) {
Long idAsLong;
if (canConvertFromStringTo(Long.class)) {
if (conversionService.canConvert(identifier.getClass(), Long.class)) {
idAsLong = conversionService.convert(identifier, Long.class);
} else {
idAsLong = Long.valueOf(identifier.toString());
@@ -577,6 +577,25 @@ public class AclImplTests {
assertThat(acl.isGranted(permissions, sids, false)).isTrue();
}
@Test
public void hashCodeWithoutStackOverFlow() throws Exception {
//given
Sid sid = new PrincipalSid("pSid");
ObjectIdentity oid = new ObjectIdentityImpl("type", 1);
AclAuthorizationStrategy authStrategy = new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("role"));
PermissionGrantingStrategy grantingStrategy = new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
AclImpl acl = new AclImpl(oid, 1L, authStrategy, grantingStrategy, null, null, false, sid);
AccessControlEntryImpl ace = new AccessControlEntryImpl(1L, acl, sid, BasePermission.READ, true, true, true);
Field fieldAces = FieldUtils.getField(AclImpl.class, "aces");
fieldAces.setAccessible(true);
List<AccessControlEntryImpl> aces = (List<AccessControlEntryImpl>) fieldAces.get(acl);
aces.add(ace);
//when - then none StackOverFlowError been raised
ace.hashCode();
}
// ~ Inner Classes
// ==================================================================================================
@@ -24,6 +24,7 @@ import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.core.convert.ConversionService;
import java.io.Serializable;
import java.math.BigInteger;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
@@ -39,6 +40,7 @@ import static org.mockito.BDDMockito.given;
public class AclClassIdUtilsTest {
private static final Long DEFAULT_IDENTIFIER = 999L;
private static final BigInteger BIGINT_IDENTIFIER = new BigInteger("999");
private static final String DEFAULT_IDENTIFIER_AS_STRING = DEFAULT_IDENTIFIER.toString();
@Mock
@@ -62,6 +64,15 @@ public class AclClassIdUtilsTest {
assertThat(newIdentifier).isEqualTo(DEFAULT_IDENTIFIER);
}
@Test
public void shouldReturnLongIfIdentifierIsBigInteger() throws SQLException {
// when
Serializable newIdentifier = aclClassIdUtils.identifierFrom(BIGINT_IDENTIFIER, resultSet);
// then
assertThat(newIdentifier).isEqualTo(DEFAULT_IDENTIFIER);
}
@Test
public void shouldReturnLongIfClassIdTypeIsNull() throws SQLException {
// given
+1 -1
View File
@@ -2,7 +2,7 @@ buildscript {
dependencies {
classpath 'io.spring.gradle:spring-build-conventions:0.0.23.RELEASE'
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
classpath 'io.spring.nohttp:nohttp-gradle:0.0.2.RELEASE'
classpath 'io.spring.nohttp:nohttp-gradle:0.0.5.RELEASE'
classpath "io.freefair.gradle:aspectj-plugin:4.0.2"
}
repositories {
@@ -47,7 +47,7 @@ public class ServiceAuthenticationDetailsSource implements
// ===================================================================================================
/**
* Creates an implementation that uses the specified ServiceProperites and the default
* Creates an implementation that uses the specified ServiceProperties and the default
* CAS artifactParameterName.
*
* @param serviceProperties The ServiceProperties to use to construct the serviceUrl.
@@ -16,8 +16,10 @@
package org.springframework.security.config.annotation.configuration;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -34,9 +36,11 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
* @since 3.2
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ObjectPostProcessorConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public ObjectPostProcessor<Object> objectPostProcessor(
AutowireCapableBeanFactory beanFactory) {
return new AutowireBeanFactoryObjectPostProcessor(beanFactory);
@@ -29,6 +29,7 @@ import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.*;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
@@ -80,6 +81,7 @@ import org.springframework.util.Assert;
* @see EnableGlobalMethodSecurity
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class GlobalMethodSecurityConfiguration
implements ImportAware, SmartInitializingSingleton, BeanFactoryAware {
private static final Log logger = LogFactory
@@ -15,14 +15,18 @@
*/
package org.springframework.security.config.annotation.method.configuration;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource;
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
class Jsr250MetadataSourceConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource() {
return new Jsr250MethodSecurityMetadataSource();
}
@@ -54,6 +54,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public DelegatingMethodSecurityMetadataSource methodMetadataSource(MethodSecurityExpressionHandler methodSecurityExpressionHandler) {
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
methodSecurityExpressionHandler);
@@ -74,6 +75,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
if (this.grantedAuthorityDefaults != null) {
@@ -1105,6 +1105,7 @@ public final class HttpSecurity extends
/**
* Allows restricting access based upon the {@link HttpServletRequest} using
* {@link RequestMatcher} implementations (i.e. via URL patterns).
*
* <h2>Example Configurations</h2>
*
@@ -1471,7 +1472,7 @@ public final class HttpSecurity extends
* }
* </pre>
*
* @return the {@link ServletApiConfigurer} for further customizations
* @return the {@link CsrfConfigurer} for further customizations
* @throws Exception
*/
public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -31,6 +31,7 @@ import org.springframework.context.ApplicationContextAware;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.SecurityBuilder;
@@ -74,6 +75,7 @@ import org.springframework.web.filter.DelegatingFilterProxy;
* @see WebSecurityConfiguration
*
* @author Rob Winch
* @author Evgeniy Cheban
* @since 3.2
*/
public final class WebSecurity extends
@@ -383,6 +385,11 @@ public final class WebSecurity extends
throws BeansException {
this.defaultWebSecurityExpressionHandler
.setApplicationContext(applicationContext);
try {
this.defaultWebSecurityExpressionHandler.setRoleHierarchy(applicationContext.getBean(RoleHierarchy.class));
} catch (NoSuchBeanDefinitionException e) {}
try {
this.defaultWebSecurityExpressionHandler.setPermissionEvaluator(applicationContext.getBean(
PermissionEvaluator.class));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -23,6 +23,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import reactor.core.CoreSubscriber;
@@ -92,32 +93,21 @@ class SecurityReactorContextConfiguration {
}
private static boolean contextAttributesAvailable() {
HttpServletRequest servletRequest = null;
HttpServletResponse servletResponse = null;
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
servletRequest = requestAttributes.getRequest();
servletResponse = requestAttributes.getResponse();
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null || servletRequest != null || servletResponse != null) {
return true;
}
return false;
return SecurityContextHolder.getContext().getAuthentication() != null ||
RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes;
}
private static Map<Object, Object> getContextAttributes() {
HttpServletRequest servletRequest = null;
HttpServletResponse servletResponse = null;
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
servletRequest = requestAttributes.getRequest();
servletResponse = requestAttributes.getResponse();
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
servletRequest = servletRequestAttributes.getRequest();
servletResponse = servletRequestAttributes.getResponse(); // possible null
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null && servletRequest == null && servletResponse == null) {
if (authentication == null && servletRequest == null) {
return Collections.emptyMap();
}
@@ -330,6 +330,13 @@ public abstract class WebSecurityConfigurerAdapter implements
/**
* Override this method to configure {@link WebSecurity}. For example, if you wish to
* ignore certain requests.
*
* Endpoints specified in this method will be ignored by Spring Security, meaning it
* will not protect them from CSRF, XSS, Clickjacking, and so on.
*
* Instead, if you want to protect endpoints against common vulnerabilities, then see
* {@link #configure(HttpSecurity)} and the {@link HttpSecurity#authorizeRequests}
* configuration method.
*/
public void configure(WebSecurity web) throws Exception {
}
@@ -343,6 +350,10 @@ public abstract class WebSecurityConfigurerAdapter implements
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
* </pre>
*
* Any endpoint that requires defense against common vulnerabilities can be specified here, including public ones.
* See {@link HttpSecurity#authorizeRequests} and the `permitAll()` authorization rule
* for more details on public endpoints.
*
* @param http the {@link HttpSecurity} to modify
* @throws Exception if an error occurs
*/
@@ -172,7 +172,7 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
/**
* Specifies the {@link AuthenticationSuccessHandler} to be used. The default is
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properites
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properties
* set.
*
* @param successHandler the {@link AuthenticationSuccessHandler}.
@@ -162,6 +162,7 @@ public final class RequestCacheConfigurer<H extends HttpSecurityBuilder<H>> exte
matchers.add(notMatchingMediaType(http, MediaType.APPLICATION_JSON));
matchers.add(notXRequestedWith);
matchers.add(notMatchingMediaType(http, MediaType.MULTIPART_FORM_DATA));
matchers.add(notMatchingMediaType(http, MediaType.TEXT_EVENT_STREAM));
return new AndRequestMatcher(matchers);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -222,9 +222,16 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
@Override
public void init(H http) {
validateConfiguration();
registerDefaultAccessDeniedHandler(http);
registerDefaultEntryPoint(http);
registerDefaultCsrfOverride(http);
AuthenticationProvider authenticationProvider = getAuthenticationProvider();
if (authenticationProvider != null) {
http.authenticationProvider(authenticationProvider);
}
}
@Override
@@ -232,8 +239,6 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
BearerTokenResolver bearerTokenResolver = getBearerTokenResolver();
this.requestMatcher.setBearerTokenResolver(bearerTokenResolver);
validateConfiguration();
AuthenticationManagerResolver resolver = this.authenticationManagerResolver;
if (resolver == null) {
AuthenticationManager authenticationManager = getAuthenticationManager(http);
@@ -321,9 +326,9 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
return this.decoder;
}
AuthenticationManager getAuthenticationManager(H http) {
AuthenticationProvider getAuthenticationProvider() {
if (this.authenticationManager != null) {
return this.authenticationManager;
return null;
}
JwtDecoder decoder = getJwtDecoder();
@@ -333,9 +338,13 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
JwtAuthenticationProvider provider =
new JwtAuthenticationProvider(decoder);
provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
AuthenticationProvider authenticationProvider = postProcess(provider);
return postProcess(provider);
}
http.authenticationProvider(authenticationProvider);
AuthenticationManager getAuthenticationManager(H http) {
if (this.authenticationManager != null) {
return this.authenticationManager;
}
return http.getSharedObject(AuthenticationManager.class);
}
@@ -391,16 +400,19 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
return this.context.getBean(OpaqueTokenIntrospector.class);
}
AuthenticationProvider getAuthenticationProvider() {
if (this.authenticationManager != null) {
return null;
}
OpaqueTokenIntrospector introspector = getIntrospector();
return new OpaqueTokenAuthenticationProvider(introspector);
}
AuthenticationManager getAuthenticationManager(H http) {
if (this.authenticationManager != null) {
return this.authenticationManager;
}
OpaqueTokenIntrospector introspector = getIntrospector();
OpaqueTokenAuthenticationProvider provider =
new OpaqueTokenAuthenticationProvider(introspector);
http.authenticationProvider(provider);
return http.getSharedObject(AuthenticationManager.class);
}
}
@@ -439,6 +451,18 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
csrf.ignoringRequestMatchers(this.requestMatcher);
}
AuthenticationProvider getAuthenticationProvider() {
if (this.jwtConfigurer != null) {
return this.jwtConfigurer.getAuthenticationProvider();
}
if (this.opaqueTokenConfigurer != null) {
return this.opaqueTokenConfigurer.getAuthenticationProvider();
}
return null;
}
AuthenticationManager getAuthenticationManager(H http) {
if (this.jwtConfigurer != null) {
return this.jwtConfigurer.getAuthenticationManager(http);
@@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web.reactive;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -87,11 +88,11 @@ class ServerHttpSecurityConfiguration {
@Bean
public WebFluxConfigurer authenticationPrincipalArgumentResolverConfigurer(
AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver) {
ObjectProvider<AuthenticationPrincipalArgumentResolver> authenticationPrincipalArgumentResolver) {
return new WebFluxConfigurer() {
@Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
configurer.addCustomResolver(authenticationPrincipalArgumentResolver);
configurer.addCustomResolver(authenticationPrincipalArgumentResolver.getObject());
}
};
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -33,6 +33,8 @@ import java.util.function.Supplier;
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.server.WebSessionOAuth2ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
@@ -55,6 +57,7 @@ import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
@@ -76,9 +79,11 @@ import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserSer
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationCodeGrantWebFilter;
import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationRequestRedirectWebFilter;
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationCodeAuthenticationTokenConverter;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.WebSessionOAuth2ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
@@ -103,6 +108,7 @@ import org.springframework.security.web.server.DelegatingServerAuthenticationEnt
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
@@ -984,9 +990,9 @@ public class ServerHttpSecurity {
private ServerWebExchangeMatcher authenticationMatcher;
private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
private ServerAuthenticationFailureHandler authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception);
private ServerAuthenticationFailureHandler authenticationFailureHandler;
/**
* Configures the {@link ReactiveAuthenticationManager} to use. The default is
@@ -1028,6 +1034,7 @@ public class ServerHttpSecurity {
/**
* The {@link ServerAuthenticationFailureHandler} used after authentication failure.
* Defaults to {@link RedirectServerAuthenticationFailureHandler} redirecting to "/login?error".
*
* @since 5.2
* @param authenticationFailureHandler the failure handler to use
@@ -1084,7 +1091,13 @@ public class ServerHttpSecurity {
private ServerAuthenticationConverter getAuthenticationConverter(ReactiveClientRegistrationRepository clientRegistrationRepository) {
if (this.authenticationConverter == null) {
this.authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository);
ServerOAuth2AuthorizationCodeAuthenticationTokenConverter delegate =
new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository);
delegate.setAuthorizationRequestRepository(getAuthorizationRequestRepository());
ServerAuthenticationConverter authenticationConverter = exchange ->
delegate.convert(exchange).onErrorMap(OAuth2AuthorizationException.class,
e -> new OAuth2AuthenticationException(e.getError(), e.getError().toString()));
this.authenticationConverter = authenticationConverter;
}
return this.authenticationConverter;
}
@@ -1172,24 +1185,76 @@ public class ServerHttpSecurity {
authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher());
authenticationFilter.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
authenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
authenticationFilter.setAuthenticationFailureHandler(this.authenticationFailureHandler);
authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
authenticationFilter.setAuthenticationFailureHandler(getAuthenticationFailureHandler());
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
MediaType.TEXT_HTML);
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
Map<String, String> urlToText = http.oauth2Login.getLinks();
if (urlToText.size() == 1) {
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint(urlToText.keySet().iterator().next())));
} else {
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint("/login")));
}
setDefaultEntryPoints(http);
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
}
private void setDefaultEntryPoints(ServerHttpSecurity http) {
String defaultLoginPage = "/login";
Map<String, String> urlToText = http.oauth2Login.getLinks();
String providerLoginPage = null;
if (urlToText.size() == 1) {
providerLoginPage = urlToText.keySet().iterator().next();
}
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"),
MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
ServerWebExchangeMatcher xhrMatcher = exchange -> {
if (exchange.getRequest().getHeaders().getOrEmpty("X-Requested-With").contains("XMLHttpRequest")) {
return ServerWebExchangeMatcher.MatchResult.match();
}
return ServerWebExchangeMatcher.MatchResult.notMatch();
};
ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(
notXhrMatcher, htmlMatcher);
if (providerLoginPage != null) {
ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher(defaultLoginPage);
ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher("/favicon.ico");
ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher(
new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(
notXhrMatcher, new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher));
RedirectServerAuthenticationEntryPoint entryPoint =
new RedirectServerAuthenticationEntryPoint(providerLoginPage);
entryPoint.setRequestCache(http.requestCache.requestCache);
http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint));
}
RedirectServerAuthenticationEntryPoint defaultEntryPoint =
new RedirectServerAuthenticationEntryPoint(defaultLoginPage);
defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
}
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
if (this.authenticationSuccessHandler == null) {
RedirectServerAuthenticationSuccessHandler handler = new RedirectServerAuthenticationSuccessHandler();
handler.setRequestCache(http.requestCache.requestCache);
this.authenticationSuccessHandler = handler;
}
return this.authenticationSuccessHandler;
}
private ServerAuthenticationFailureHandler getAuthenticationFailureHandler() {
if (this.authenticationFailureHandler == null) {
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler("/login?error");
}
return this.authenticationFailureHandler;
}
private ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() {
return new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}");
}
@@ -1766,6 +1831,28 @@ public class ServerHttpSecurity {
}
}
private class BearerTokenAuthenticationWebFilter extends AuthenticationWebFilter {
private ServerAuthenticationFailureHandler authenticationFailureHandler;
BearerTokenAuthenticationWebFilter(ReactiveAuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
return super.filter(exchange, chain)
.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler
.onAuthenticationFailure(webFilterExchange, e));
}
@Override
public void setAuthenticationFailureHandler(ServerAuthenticationFailureHandler authenticationFailureHandler) {
super.setAuthenticationFailureHandler(authenticationFailureHandler);
this.authenticationFailureHandler = authenticationFailureHandler;
}
}
/**
* Configures JWT Resource Server Support
*/
@@ -1839,7 +1926,7 @@ public class ServerHttpSecurity {
protected void configure(ServerHttpSecurity http) {
ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
AuthenticationWebFilter oauth2 = new BearerTokenAuthenticationWebFilter(authenticationManager);
oauth2.setServerAuthenticationConverter(bearerTokenConverter);
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
http
@@ -1945,7 +2032,7 @@ public class ServerHttpSecurity {
protected void configure(ServerHttpSecurity http) {
ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
AuthenticationWebFilter oauth2 = new BearerTokenAuthenticationWebFilter(authenticationManager);
oauth2.setServerAuthenticationConverter(bearerTokenConverter);
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
@@ -2568,7 +2655,7 @@ public class ServerHttpSecurity {
/**
* Require a specific authority.
* @param authority the authority to require (i.e. "USER" woudl require authority of "USER").
* @param authority the authority to require (i.e. "USER" would require authority of "USER").
* @return the {@link AuthorizeExchangeSpec} to configure
*/
public AuthorizeExchangeSpec hasAuthority(String authority) {
@@ -2765,7 +2852,9 @@ public class ServerHttpSecurity {
protected void configure(ServerHttpSecurity http) {
if (this.csrfTokenRepository != null) {
this.filter.setCsrfTokenRepository(this.csrfTokenRepository);
http.logout().addLogoutHandler(new CsrfServerLogoutHandler(this.csrfTokenRepository));
if (ServerHttpSecurity.this.logout != null) {
ServerHttpSecurity.this.logout.addLogoutHandler(new CsrfServerLogoutHandler(this.csrfTokenRepository));
}
}
http.addFilterAt(this.filter, SecurityWebFiltersOrder.CSRF);
}
@@ -3022,8 +3111,12 @@ public class ServerHttpSecurity {
public FormLoginSpec loginPage(String loginPage) {
this.defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(loginPage);
this.authenticationEntryPoint = this.defaultEntryPoint;
this.requiresAuthenticationMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, loginPage);
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler(loginPage + "?error");
if (this.requiresAuthenticationMatcher == null) {
this.requiresAuthenticationMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, loginPage);
}
if (this.authenticationFailureHandler == null) {
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler(loginPage + "?error");
}
return this;
}
@@ -3640,7 +3733,8 @@ public class ServerHttpSecurity {
*/
public final class LogoutSpec {
private LogoutWebFilter logoutWebFilter = new LogoutWebFilter();
private List<ServerLogoutHandler> logoutHandlers = new ArrayList<>(Arrays.asList(new SecurityContextServerLogoutHandler()));
private final SecurityContextServerLogoutHandler DEFAULT_LOGOUT_HANDLER = new SecurityContextServerLogoutHandler();
private List<ServerLogoutHandler> logoutHandlers = new ArrayList<>(Arrays.asList(this.DEFAULT_LOGOUT_HANDLER));
/**
* Configures the logout handler. Default is {@code SecurityContextServerLogoutHandler}
@@ -3704,6 +3798,10 @@ public class ServerHttpSecurity {
}
private ServerLogoutHandler createLogoutHandler() {
ServerSecurityContextRepository securityContextRepository = ServerHttpSecurity.this.securityContextRepository;
if (securityContextRepository != null) {
this.DEFAULT_LOGOUT_HANDLER.setSecurityContextRepository(securityContextRepository);
}
if (this.logoutHandlers.isEmpty()) {
return null;
} else if (this.logoutHandlers.size() == 1) {
@@ -103,7 +103,9 @@ public class RSocketMessageHandlerITests {
.data(data)
.retrieveMono(String.class)
.block()
).isInstanceOf(ApplicationErrorException.class);
).isInstanceOf(ApplicationErrorException.class)
.hasMessageContaining("Access Denied");
assertThat(this.controller.payloads).isEmpty();
}
@@ -116,7 +118,9 @@ public class RSocketMessageHandlerITests {
.data(data)
.retrieveMono(String.class)
.block()
).isInstanceOf(ApplicationErrorException.class);
).isInstanceOf(ApplicationErrorException.class)
.hasMessageContaining("Invalid Credentials");
assertThat(this.controller.payloads).isEmpty();
}
@@ -149,12 +153,13 @@ public class RSocketMessageHandlerITests {
@Test
public void retrieveFluxWhenDataFluxAndSecureThenDenied() throws Exception {
Flux<String> data = Flux.just("a", "b", "c");
assertThatCode(() -> this.requester.route("secure.secure.retrieve-flux")
assertThatCode(() -> this.requester.route("secure.retrieve-flux")
.data(data, String.class)
.retrieveFlux(String.class)
.collectList()
.block()).isInstanceOf(
ApplicationErrorException.class);
.block()
).isInstanceOf(ApplicationErrorException.class)
.hasMessageContaining("Access Denied");
assertThat(this.controller.payloads).isEmpty();
}
@@ -179,8 +184,9 @@ public class RSocketMessageHandlerITests {
.data(data)
.retrieveFlux(String.class)
.collectList()
.block()).isInstanceOf(
ApplicationErrorException.class);
.block()
).isInstanceOf(ApplicationErrorException.class)
.hasMessageContaining("Access Denied");
assertThat(this.controller.payloads).isEmpty();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -28,6 +28,7 @@ import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.web.reactive.function.client.MockExchangeFunction;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.reactive.function.client.ClientRequest;
@@ -36,6 +37,7 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Operators;
import reactor.test.StepVerifier;
import reactor.util.context.Context;
@@ -139,6 +141,52 @@ public class SecurityReactorContextConfigurationTests {
assertThat(resultContext).isSameAs(parentContext);
}
@Test
public void createSubscriberIfNecessaryWhenNotServletRequestAttributesThenStillCreate() {
RequestContextHolder.setRequestAttributes(
new RequestAttributes() {
@Override
public Object getAttribute(String name, int scope) {
return null;
}
@Override
public void setAttribute(String name, Object value, int scope) {
}
@Override
public void removeAttribute(String name, int scope) {
}
@Override
public String[] getAttributeNames(int scope) {
return new String[0];
}
@Override
public void registerDestructionCallback(String name, Runnable callback, int scope) {
}
@Override
public Object resolveReference(String key) {
return null;
}
@Override
public String getSessionId() {
return null;
}
@Override
public Object getSessionMutex() {
return null;
}
});
CoreSubscriber<Object> subscriber = this.subscriberRegistrar.createSubscriberIfNecessary(Operators.emptySubscriber());
assertThat(subscriber).isInstanceOf(SecurityReactorContextConfiguration.SecurityReactorContextSubscriber.class);
}
@Test
public void createPublisherWhenLastOperatorAddedThenSecurityContextAttributesAvailable() {
// Trigger the importing of SecurityReactorContextConfiguration via OAuth2ImportSelector
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -32,6 +32,8 @@ import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.AbstractSecurityExpressionHandler;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
@@ -69,6 +71,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
*
* @author Rob Winch
* @author Joe Grandja
* @author Evgeniy Cheban
*/
public class WebSecurityConfigurationTests {
@Rule
@@ -290,6 +293,31 @@ public class WebSecurityConfigurationTests {
}
}
@Test
public void securityExpressionHandlerWhenRoleHierarchyBeanThenRoleHierarchyUsed() {
this.spring.register(WebSecurityExpressionHandlerRoleHierarchyBeanConfig.class).autowire();
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "notused", "ROLE_ADMIN");
FilterInvocation invocation = new FilterInvocation(new MockHttpServletRequest("GET", ""),
new MockHttpServletResponse(), new MockFilterChain());
AbstractSecurityExpressionHandler handler = this.spring.getContext().getBean(AbstractSecurityExpressionHandler.class);
EvaluationContext evaluationContext = handler.createEvaluationContext(authentication, invocation);
Expression expression = handler.getExpressionParser()
.parseExpression("hasRole('ROLE_USER')");
boolean granted = expression.getValue(evaluationContext, Boolean.class);
assertThat(granted).isTrue();
}
@EnableWebSecurity
static class WebSecurityExpressionHandlerRoleHierarchyBeanConfig extends WebSecurityConfigurerAdapter {
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return roleHierarchy;
}
}
@Test
public void securityExpressionHandlerWhenPermissionEvaluatorBeanThenPermissionEvaluatorUsed() {
this.spring.register(WebSecurityExpressionHandlerPermissionEvaluatorBeanConfig.class).autowire();
@@ -458,4 +458,25 @@ public class LogoutConfigurerTests {
@EnableWebSecurity
static class BasicSecurityConfig extends WebSecurityConfigurerAdapter {
}
@Test
public void logoutWhenDisabledThenLogoutUrlNotFound() throws Exception {
this.spring.register(LogoutDisabledConfig.class).autowire();
this.mvc.perform(post("/logout")
.with(csrf()))
.andExpect(status().isNotFound());
}
@EnableWebSecurity
static class LogoutDisabledConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.logout()
.disable();
// @formatter:on
}
}
}
@@ -183,6 +183,21 @@ public class RequestCacheConfigurerTests {
// This is desirable since XHR requests are typically not invoked directly from the browser and we don't want the browser to replay them
}
@Test
public void getWhenBookmarkedRequestIsTextEventStreamThenPostAuthenticationRedirectsToRoot() throws Exception {
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
MockHttpSession session = (MockHttpSession)
this.mvc.perform(get("/messages")
.header(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM))
.andExpect(redirectedUrl("http://localhost/login"))
.andReturn().getRequest().getSession();
this.mvc.perform(formLogin(session))
.andExpect(redirectedUrl("/")); // ignores text/event-stream
// This is desirable since event-stream requests are typically not invoked directly from the browser and we don't want the browser to replay them
}
@Test
public void getWhenBookmarkedRequestIsAllMediaTypeThenPostAuthenticationRemembers() throws Exception {
@@ -22,6 +22,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationTrustResolver;
@@ -44,10 +45,14 @@ import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import javax.servlet.Filter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -60,6 +65,7 @@ import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -329,6 +335,39 @@ public class ServletApiConfigurerTests {
}
}
@Test
public void logoutServletApiWhenCsrfDisabled() throws Exception {
ConfigurableWebApplicationContext context = this.spring.register(CsrfDisabledConfig.class).getContext();
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(springSecurity())
.build();
MvcResult mvcResult = mockMvc.perform(get("/"))
.andReturn();
assertThat(mvcResult.getRequest().getSession(false)).isNull();
}
@Configuration
@EnableWebSecurity
static class CsrfDisabledConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.csrf().disable();
// @formatter:on
}
@RestController
static class LogoutController {
@GetMapping("/")
String logout(HttpServletRequest request) throws ServletException {
request.getSession().setAttribute("foo", "bar");
request.logout();
return "logout";
}
}
}
private <T extends Filter> T getFilter(Class<T> filterClass) {
return (T) getFilters().stream()
.filter(filterClass::isInstance)
@@ -360,6 +360,18 @@ public class OAuth2ResourceServerConfigurerTests {
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer"));
}
// gh-8031
@Test
public void getWhenAnonymousDisabledThenAllows() throws Exception {
this.spring.register(RestOperationsConfig.class, AnonymousDisabledConfig.class).autowire();
mockRestOperations(jwks("Default"));
String token = token("ValidNoScopes");
this.mvc.perform(get("/authenticated")
.with(bearerToken(token)))
.andExpect(status().isNotFound());
}
@Test
public void getWhenUsingDefaultsWithNoBearerTokenThenUnauthorized()
throws Exception {
@@ -730,7 +742,8 @@ public class OAuth2ResourceServerConfigurerTests {
@Test
public void getBearerTokenResolverWhenDuplicateResolverBeansThenWiringException() {
assertThatCode(() -> this.spring.register(MultipleBearerTokenResolverBeansConfig.class).autowire())
assertThatCode(() -> this.spring
.register(MultipleBearerTokenResolverBeansConfig.class, JwtDecoderConfig.class).autowire())
.isInstanceOf(BeanCreationException.class)
.hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class);
}
@@ -1127,7 +1140,7 @@ public class OAuth2ResourceServerConfigurerTests {
.with(bearerToken("token")))
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
containsString("Provided token [token] isn't active")));
containsString("Provided token isn't active")));
}
@Test
@@ -1469,6 +1482,22 @@ public class OAuth2ResourceServerConfigurerTests {
}
}
@EnableWebSecurity
static class AnonymousDisabledConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.anonymous().disable()
.oauth2ResourceServer()
.jwt();
// @formatter:on
}
}
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
static class MethodSecurityConfig extends WebSecurityConfigurerAdapter {
@@ -47,6 +47,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
@@ -59,6 +60,7 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.result.view.AbstractView;
@@ -434,4 +436,23 @@ public class EnableWebFluxSecurityTests {
Child() {
}
}
@Test
// gh-8596
public void resolveAuthenticationPrincipalArgumentResolverFirstDoesNotCauseBeanCurrentlyInCreationException() {
this.spring.register(EnableWebFluxSecurityConfiguration.class,
ReactiveAuthenticationTestConfiguration.class,
DelegatingWebFluxConfiguration.class).autowire();
}
@EnableWebFluxSecurity
@Configuration(proxyBeanMethods = false)
static class EnableWebFluxSecurityConfiguration {
/**
* It is necessary to Autowire AuthenticationPrincipalArgumentResolver because it triggers eager loading of
* AuthenticationPrincipalArgumentResolver bean which causes BeanCurrentlyInCreationException
*/
@Autowired
AuthenticationPrincipalArgumentResolver resolver;
}
}
@@ -17,7 +17,6 @@
package org.springframework.security.config.test;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.config.util.InMemoryXmlWebApplicationContext;
@@ -113,8 +112,10 @@ public class SpringTestContext implements Closeable {
return this;
}
public ConfigurableApplicationContext getContext() {
public ConfigurableWebApplicationContext getContext() {
if (!this.context.isRunning()) {
this.context.setServletContext(new MockServletContext());
this.context.setServletConfig(new MockServletConfig());
this.context.refresh();
}
return this.context;
@@ -33,9 +33,11 @@ import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverB
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.csrf.CsrfToken;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.stereotype.Controller;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
@@ -213,6 +215,62 @@ public class FormLoginTests {
homePage.assertAt();
}
@Test
public void formLoginWhenCustomAuthenticationFailureHandlerThenUsed() {
SecurityWebFilterChain securityWebFilter = this.http
.authorizeExchange()
.pathMatchers("/login", "/failure").permitAll()
.anyExchange().authenticated()
.and()
.formLogin()
.authenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/failure"))
.and()
.build();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(securityWebFilter)
.build();
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
.webTestClientSetup(webTestClient)
.build();
DefaultLoginPage loginPage = HomePage.to(driver, DefaultLoginPage.class)
.assertAt();
loginPage.loginForm()
.username("invalid")
.password("invalid")
.submit(HomePage.class);
assertThat(driver.getCurrentUrl()).endsWith("/failure");
}
@Test
public void formLoginWhenCustomRequiresAuthenticationMatcherThenUsed() {
SecurityWebFilterChain securityWebFilter = this.http
.authorizeExchange()
.pathMatchers("/login", "/sign-in").permitAll()
.anyExchange().authenticated()
.and()
.formLogin()
.requiresAuthenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/sign-in"))
.and()
.build();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(securityWebFilter)
.build();
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
.webTestClientSetup(webTestClient)
.build();
driver.get("http://localhost/sign-in");
assertThat(driver.getCurrentUrl()).endsWith("/login?error");
}
@Test
public void authenticationSuccess() {
SecurityWebFilterChain securityWebFilter = this.http
@@ -21,6 +21,7 @@ import org.openqa.selenium.WebDriver;
import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder;
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
@@ -164,4 +165,82 @@ public class LogoutSpecTests {
.assertAt()
.assertLogout();
}
@Test
public void logoutWhenDisabledThenPostToLogoutDoesNothing() {
SecurityWebFilterChain securityWebFilter = this.http
.authorizeExchange()
.anyExchange().authenticated()
.and()
.formLogin().and()
.logout().disable()
.build();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(securityWebFilter)
.build();
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
.webTestClientSetup(webTestClient)
.build();
FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class)
.assertAt();
FormLoginTests.HomePage homePage = loginPage.loginForm()
.username("user")
.password("password")
.submit(FormLoginTests.HomePage.class);
homePage.assertAt();
FormLoginTests.DefaultLogoutPage.to(driver)
.assertAt()
.logout();
homePage
.assertAt();
}
@Test
public void logoutWhenCustomSecurityContextRepositoryThenLogsOut() {
WebSessionServerSecurityContextRepository repository = new WebSessionServerSecurityContextRepository();
repository.setSpringSecurityContextAttrName("CUSTOM_CONTEXT_ATTR");
SecurityWebFilterChain securityWebFilter = this.http
.securityContextRepository(repository)
.authorizeExchange()
.anyExchange().authenticated()
.and()
.formLogin()
.and()
.logout()
.and()
.build();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(securityWebFilter)
.build();
WebDriver driver = WebTestClientHtmlUnitDriverBuilder
.webTestClientSetup(webTestClient)
.build();
FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class)
.assertAt();
FormLoginTests.HomePage homePage = loginPage.loginForm()
.username("user")
.password("password")
.submit(FormLoginTests.HomePage.class);
homePage.assertAt();
FormLoginTests.DefaultLogoutPage.to(driver)
.assertAt()
.logout();
FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class)
.assertAt();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
@@ -70,6 +71,7 @@ import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtValidationException;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
@@ -81,6 +83,7 @@ import org.springframework.security.web.server.authentication.RedirectServerAuth
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
@@ -100,7 +103,10 @@ import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.jwt.TestJwts.jwt;
/**
@@ -184,6 +190,22 @@ public class OAuth2LoginTests {
assertThat(driver.getCurrentUrl()).startsWith("https://github.com/login/oauth/authorize");
}
// gh-8118
@Test
public void defaultLoginPageWithSingleClientRegistrationAndXhrRequestThenDoesNotRedirectForAuthorization() {
this.spring.register(OAuth2LoginWithSingleClientRegistrations.class, WebFluxConfig.class).autowire();
this.client.get()
.uri("/")
.header("X-Requested-With", "XMLHttpRequest")
.exchange()
.expectStatus().is3xxRedirection()
.expectHeader().valueEquals(HttpHeaders.LOCATION, "/login");
}
@EnableWebFlux
static class WebFluxConfig { }
@EnableWebFluxSecurity
static class OAuth2LoginWithSingleClientRegistrations {
@Bean
@@ -518,6 +540,85 @@ public class OAuth2LoginTests {
verify(securityContextRepository).save(any(), any());
}
// gh-5562
@Test
public void oauth2LoginWhenAccessTokenRequestFailsThenDefaultRedirectToLogin() {
this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class,
OAuth2LoginWithCustomBeansConfig.class).autowire();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(this.springSecurity)
.build();
OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().scope("openid").build();
OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build();
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(request, response);
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid");
OAuth2AuthorizationCodeAuthenticationToken authenticationToken = new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
OAuth2LoginWithCustomBeansConfig config = this.spring.getContext().getBean(OAuth2LoginWithCustomBeansConfig.class);
ServerAuthenticationConverter converter = config.authenticationConverter;
when(converter.convert(any())).thenReturn(Mono.just(authenticationToken));
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient = config.tokenResponseClient;
OAuth2Error oauth2Error = new OAuth2Error("invalid_request", "Invalid request", null);
when(tokenResponseClient.getTokenResponse(any())).thenThrow(new OAuth2AuthenticationException(oauth2Error));
webTestClient.get()
.uri("/login/oauth2/code/google")
.exchange()
.expectStatus()
.is3xxRedirection()
.expectHeader()
.valueEquals("Location", "/login?error");
}
// gh-6484
@Test
public void oauth2LoginWhenIdTokenValidationFailsThenDefaultRedirectToLogin() {
this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class,
OAuth2LoginWithCustomBeansConfig.class).autowire();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(this.springSecurity)
.build();
OAuth2LoginWithCustomBeansConfig config = this.spring.getContext().getBean(OAuth2LoginWithCustomBeansConfig.class);
OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().scope("openid").build();
OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build();
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(request, response);
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid");
OAuth2AuthorizationCodeAuthenticationToken authenticationToken = new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
ServerAuthenticationConverter converter = config.authenticationConverter;
when(converter.convert(any())).thenReturn(Mono.just(authenticationToken));
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token");
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.tokenType(accessToken.getTokenType())
.scopes(accessToken.getScopes())
.additionalParameters(additionalParameters)
.build();
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient = config.tokenResponseClient;
when(tokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = config.jwtDecoderFactory;
OAuth2Error oauth2Error = new OAuth2Error("invalid_id_token", "Invalid ID Token", null);
when(jwtDecoderFactory.createDecoder(any())).thenReturn(token ->
Mono.error(new JwtValidationException("ID Token validation failed", Collections.singleton(oauth2Error))));
webTestClient.get()
.uri("/login/oauth2/code/google")
.exchange()
.expectStatus()
.is3xxRedirection()
.expectHeader()
.valueEquals("Location", "/login?error");
}
@Configuration
static class OAuth2LoginWithCustomBeansConfig {
@@ -585,7 +686,6 @@ public class OAuth2LoginTests {
}
}
@Test
public void logoutWhenUsingOidcLogoutHandlerThenRedirects() {
this.spring.register(OAuth2LoginConfigWithOidcLogoutSuccessHandler.class).autowire();
@@ -619,6 +719,8 @@ public class OAuth2LoginTests {
http
.csrf().disable()
.logout()
// avoid using mock ServerSecurityContextRepository for logout
.logoutHandler(new SecurityContextServerLogoutHandler())
.logoutSuccessHandler(
new OidcClientInitiatedServerLogoutSuccessHandler(
new InMemoryReactiveClientRegistrationRepository(this.withLogout)))
@@ -639,6 +741,24 @@ public class OAuth2LoginTests {
}
}
// gh-8609
@Test
public void oauth2LoginWhenAuthenticationConverterFailsThenDefaultRedirectToLogin() {
this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class).autowire();
WebTestClient webTestClient = WebTestClientBuilder
.bindToWebFilters(this.springSecurity)
.build();
webTestClient.get()
.uri("/login/oauth2/code/google")
.exchange()
.expectStatus()
.is3xxRedirection()
.expectHeader()
.valueEquals("Location", "/login?error");
}
static class GitHubWebFilter implements WebFilter {
@Override
@@ -172,6 +172,17 @@ public class OAuth2ResourceServerSpecTests {
.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"invalid_token\""));
}
@Test
public void getWhenEmptyBearerTokenThenReturnsInvalidToken() {
this.spring.register(PublicKeyConfig.class).autowire();
this.client.get()
.headers(headers -> headers.add("Authorization", "Bearer "))
.exchange()
.expectStatus().isUnauthorized()
.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"invalid_token\""));
}
@Test
public void getWhenValidTokenAndPublicKeyInLambdaThenReturnsOk() {
this.spring.register(PublicKeyInLambdaConfig.class, RootController.class).autowire();
@@ -20,10 +20,12 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.test.util.ReflectionTestUtils.getField;
import java.util.Arrays;
import java.util.List;
@@ -35,12 +37,20 @@ import org.apache.http.HttpHeaders;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;
import reactor.core.publisher.Mono;
import reactor.test.publisher.TestPublisher;
@@ -60,7 +70,6 @@ import org.springframework.security.web.server.context.WebSessionServerSecurityC
import org.springframework.security.web.server.csrf.CsrfServerLogoutHandler;
import org.springframework.security.web.server.csrf.CsrfWebFilter;
import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.reactive.server.EntityExchangeResult;
import org.springframework.test.web.reactive.server.FluxExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
@@ -196,7 +205,7 @@ public class ServerHttpSecurityTests {
.isNotPresent();
Optional<ServerLogoutHandler> logoutHandler = getWebFilter(securityWebFilterChain, LogoutWebFilter.class)
.map(logoutWebFilter -> (ServerLogoutHandler) ReflectionTestUtils.getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
.map(logoutWebFilter -> (ServerLogoutHandler) getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
assertThat(logoutHandler)
.get()
@@ -209,17 +218,17 @@ public class ServerHttpSecurityTests {
assertThat(getWebFilter(securityWebFilterChain, CsrfWebFilter.class))
.get()
.extracting(csrfWebFilter -> ReflectionTestUtils.getField(csrfWebFilter, "csrfTokenRepository"))
.extracting(csrfWebFilter -> getField(csrfWebFilter, "csrfTokenRepository"))
.isEqualTo(this.csrfTokenRepository);
Optional<ServerLogoutHandler> logoutHandler = getWebFilter(securityWebFilterChain, LogoutWebFilter.class)
.map(logoutWebFilter -> (ServerLogoutHandler) ReflectionTestUtils.getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
.map(logoutWebFilter -> (ServerLogoutHandler) getField(logoutWebFilter, LogoutWebFilter.class, "logoutHandler"));
assertThat(logoutHandler)
.get()
.isExactlyInstanceOf(DelegatingServerLogoutHandler.class)
.extracting(delegatingLogoutHandler ->
((List<ServerLogoutHandler>) ReflectionTestUtils.getField(delegatingLogoutHandler, DelegatingServerLogoutHandler.class, "delegates")).stream()
((List<ServerLogoutHandler>) getField(delegatingLogoutHandler, DelegatingServerLogoutHandler.class, "delegates")).stream()
.map(ServerLogoutHandler::getClass)
.collect(Collectors.toList()))
.isEqualTo(Arrays.asList(SecurityContextServerLogoutHandler.class, CsrfServerLogoutHandler.class));
@@ -475,9 +484,58 @@ public class ServerHttpSecurityTests {
verify(customServerCsrfTokenRepository).loadToken(any());
}
@Test
public void shouldConfigureRequestCacheForOAuth2LoginAuthenticationEntryPointAndSuccessHandler() {
ServerRequestCache requestCache = spy(new WebSessionServerRequestCache());
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
SecurityWebFilterChain securityFilterChain = this.http
.oauth2Login()
.clientRegistrationRepository(clientRegistrationRepository)
.and()
.authorizeExchange().anyExchange().authenticated()
.and()
.requestCache(c -> c.requestCache(requestCache))
.build();
WebTestClient client = WebTestClientBuilder.bindToWebFilters(securityFilterChain).build();
client.get().uri("/test").exchange();
ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor.forClass(ServerWebExchange.class);
verify(requestCache).saveRequest(captor.capture());
assertThat(captor.getValue().getRequest().getURI().toString()).isEqualTo("/test");
OAuth2LoginAuthenticationWebFilter authenticationWebFilter =
getWebFilter(securityFilterChain, OAuth2LoginAuthenticationWebFilter.class).get();
Object handler = getField(authenticationWebFilter, "authenticationSuccessHandler");
assertThat(getField(handler, "requestCache")).isSameAs(requestCache);
}
@Test
public void shouldConfigureAuthorizationRequestRepositoryForOAuth2Login() {
ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository = mock(ServerAuthorizationRequestRepository.class);
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request().build();
when(authorizationRequestRepository.removeAuthorizationRequest(any())).thenReturn(Mono.just(authorizationRequest));
SecurityWebFilterChain securityFilterChain = this.http
.oauth2Login()
.clientRegistrationRepository(clientRegistrationRepository)
.authorizationRequestRepository(authorizationRequestRepository)
.and()
.build();
WebTestClient client = WebTestClientBuilder.bindToWebFilters(securityFilterChain).build();
client.get().uri("/login/oauth2/code/registration-id").exchange();
verify(authorizationRequestRepository).removeAuthorizationRequest(any());
}
private boolean isX509Filter(WebFilter filter) {
try {
Object converter = ReflectionTestUtils.getField(filter, "authenticationConverter");
Object converter = getField(filter, "authenticationConverter");
return converter.getClass().isAssignableFrom(ServerX509AuthenticationConverter.class);
} catch (IllegalArgumentException e) {
// field doesn't exist
@@ -55,7 +55,7 @@ public abstract class AbstractUserDetailsReactiveAuthenticationManager implement
private ReactiveUserDetailsPasswordService userDetailsPasswordService;
private Scheduler scheduler = Schedulers.newParallel("password-encoder", Schedulers.DEFAULT_POOL_SIZE, true);
private Scheduler scheduler = Schedulers.boundedElastic();
private UserDetailsChecker preAuthenticationChecks = user -> {
if (!user.isAccountNonLocked()) {
@@ -213,7 +213,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
@@ -230,7 +230,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
@@ -129,7 +129,7 @@ import org.springframework.util.Assert;
* &lt;/property&gt;
* </pre>
*
* A configuration note: The JaasAuthenticationProvider uses the security properites
* A configuration note: The JaasAuthenticationProvider uses the security properties
* "login.config.url.X" to configure jaas. If you would like to customize the way Jaas
* gets configured, create a subclass of this and override the
* {@link #configureJaas(Resource)} method.
@@ -31,6 +31,7 @@ DigestAuthenticationFilter.usernameNotFound=Username {0} not found
JdbcDaoImpl.noAuthority=User {0} has no GrantedAuthority
JdbcDaoImpl.notFound=User {0} not found
LdapAuthenticationProvider.badCredentials=Bad credentials
LdapAuthenticationProvider.badLdapConnection=Connection to LDAP server failed
LdapAuthenticationProvider.credentialsExpired=User credentials have expired
LdapAuthenticationProvider.disabled=User is disabled
LdapAuthenticationProvider.expired=User account has expired
@@ -99,6 +99,10 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
}
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
String salt;
if (random != null) {
salt = BCrypt.gensalt(version.getVersion(), strength, random);
@@ -109,6 +113,10 @@ public class BCryptPasswordEncoder implements PasswordEncoder {
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
if (encodedPassword == null || encodedPassword.length() == 0) {
logger.warn("Empty encoded password");
return false;
@@ -1,5 +1,5 @@
/*
* Copyright 2011-2016 the original author or authors.
* Copyright 2011-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.
@@ -32,16 +32,13 @@ public class Encryptors {
* (Password-Based Key Derivation Function #2). Salts the password to prevent
* dictionary attacks against the key. The provided salt is expected to be
* hex-encoded; it should be random and at least 8 bytes in length. Also applies a
* random 16 byte initialization vector to ensure each encrypted message will be
* random 16-byte initialization vector to ensure each encrypted message will be
* unique. Requires Java 6.
*
* @param password the password used to generate the encryptor's secret key; should
* not be shared
* @param salt a hex-encoded, random, site-global salt value to use to generate the
* key
*
* @see #standard(CharSequence, CharSequence) which uses the slightly weaker CBC mode
* (instead of GCM)
*/
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
return new AesBytesEncryptor(password.toString(), salt,
@@ -53,13 +50,21 @@ public class Encryptors {
* Derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation
* Function #2). Salts the password to prevent dictionary attacks against the key. The
* provided salt is expected to be hex-encoded; it should be random and at least 8
* bytes in length. Also applies a random 16 byte initialization vector to ensure each
* bytes in length. Also applies a random 16-byte initialization vector to ensure each
* encrypted message will be unique. Requires Java 6.
* NOTE: This mode is not
* <a href="https://en.wikipedia.org/wiki/Authenticated_encryption">authenticated</a>
* and does not provide any guarantees about the authenticity of the data.
* For a more secure alternative, users should prefer
* {@link #stronger(CharSequence, CharSequence)}.
*
* @param password the password used to generate the encryptor's secret key; should
* not be shared
* @param salt a hex-encoded, random, site-global salt value to use to generate the
* key
*
* @see #stronger(CharSequence, CharSequence), which uses the significatly more secure
* GCM (instead of CBC)
*/
public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
return new AesBytesEncryptor(password.toString(), salt,
@@ -100,7 +105,10 @@ public class Encryptors {
* not be shared
* @param salt a hex-encoded, random, site-global salt value to use to generate the
* secret key
* @deprecated This encryptor is not secure. Instead, look to your data store for a
* mechanism to query encrypted data.
*/
@Deprecated
public static TextEncryptor queryableText(CharSequence password, CharSequence salt) {
return new HexEncodingTextEncryptor(new AesBytesEncryptor(password.toString(),
salt));
@@ -83,8 +83,6 @@ public class Md4PasswordEncoder implements PasswordEncoder {
private StringKeyGenerator saltGenerator = new Base64StringKeyGenerator();
private boolean encodeHashAsBase64;
private Digester digester;
public void setEncodeHashAsBase64(boolean encodeHashAsBase64) {
this.encodeHashAsBase64 = encodeHashAsBase64;
@@ -26,7 +26,8 @@ package org.springframework.security.crypto.password;
* @deprecated This PasswordEncoder is not secure. Instead use an
* adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or
* SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports
* password upgrades.
* password upgrades. There are no plans to remove this support. It is deprecated to indicate that
* this is a legacy implementation and using it is considered insecure.
*/
@Deprecated
public final class NoOpPasswordEncoder implements PasswordEncoder {
@@ -200,4 +200,16 @@ public class BCryptPasswordEncoderTests {
encoder.upgradeEncoding("not-a-bcrypt-password");
}
@Test(expected = IllegalArgumentException.class)
public void encodeNullRawPassword() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
encoder.encode(null);
}
@Test(expected = IllegalArgumentException.class)
public void matchNullRawPassword() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
encoder.matches(null, "does-not-matter");
}
}
@@ -11,6 +11,13 @@ asciidoctor {
'gh-samples-url': "$ghUrl/samples"
}
remotes {
docs {
retryCount = 5 // retry 5 times (default is 0)
retryWaitSec = 10 // wait 10 seconds between retries (default is 0)
}
}
docsZip {
from(project(':spring-security-docs-guides').asciidoctor) {
into 'guides'
@@ -38,12 +38,12 @@ This configuration enables <<rsocket-authentication-basic,basic authentication>>
For Spring Security to work we need to apply `SecuritySocketAcceptorInterceptor` to the `ServerRSocketFactory`.
This is what connects our `PayloadSocketAcceptorInterceptor` we created with the RSocket infrastructure.
In a Spring Boot application this can be done using the following code.
In a Spring Boot application this is done automatically using `RSocketSecurityAutoConfiguration` with the following code.
[source,java]
----
@Bean
ServerRSocketFactoryCustomizer springSecurityRSocketSecurity(
ServerRSocketFactoryProcessor springSecurityRSocketSecurity(
SecuritySocketAcceptorInterceptor interceptor) {
return builder -> builder.addSocketAcceptorPlugin(interceptor);
}
@@ -2372,8 +2372,8 @@ A bean identifier, used for referring to the bean elsewhere in the context.
[[nsa-ldap-server-ldif]]
* **ldif**
Explicitly specifies an ldif file resource to load into an embedded LDAP server.
The ldiff is should be a Spring resource pattern (i.e. classpath:init.ldiff).
The default is classpath*:*.ldiff
The ldif should be a Spring resource pattern (i.e. classpath:init.ldif).
The default is classpath*:*.ldif
[[nsa-ldap-server-manager-dn]]
@@ -41,7 +41,7 @@ A pre-invocation decision on whether the invocation is allowed to proceed is mad
[[authz-access-decision-manager]]
==== The AccessDecisionManager
The `AccessDecisionManager` is called by the `AbstractSecurityInterceptor` and is responsible for making final access control decisions.
the `AccessDecisionManager` interface contains three methods:
The `AccessDecisionManager` interface contains three methods:
[source,java]
----
@@ -17,14 +17,16 @@ Encryptors are thread-safe.
[[spring-security-crypto-encryption-bytes]]
=== BytesEncryptor
Use the Encryptors.standard factory method to construct a "standard" BytesEncryptor:
Use the `Encryptors.stronger` factory method to construct a BytesEncryptor:
[source,java]
----
Encryptors.standard("password", "salt");
Encryptors.stronger("password", "salt");
----
The "standard" encryption method is 256-bit AES using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
The "stronger" encryption method creates an encryptor using 256 bit AES encryption with
Galois Counter Mode (GCM).
It derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
This method requires Java 6.
The password used to generate the SecretKey should be kept in a secure place and not be shared.
The salt is used to prevent dictionary attacks against the key in the event your encrypted data is compromised.
@@ -38,6 +40,11 @@ Such a salt may be generated using a KeyGenerator:
String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded
----
Users may also use the `standard` encryption method, which is 256-bit AES in Cipher Block Chaining (CBC) Mode.
This mode is not https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated] and does not provide any
guarantees about the authenticity of the data.
For a more secure alternative, users should prefer `Encryptors.stronger`.
[[spring-security-crypto-encryption-text]]
=== TextEncryptor
Use the Encryptors.text factory method to construct a standard TextEncryptor:
@@ -343,6 +343,36 @@ private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesM
}
----
The `DefaultOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `HttpServletRequest`.
When operating *_outside_* of a `HttpServletRequest` context, use `AuthorizedClientServiceOAuth2AuthorizedClientManager` instead.
A _service application_ is a common use case for when to use an `AuthorizedClientServiceOAuth2AuthorizedClientManager`.
Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account.
An OAuth 2.0 Client configured with the `client_credentials` grant type can be considered a type of service application.
The following code shows an example of how to configure an `AuthorizedClientServiceOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type:
[source,java]
----
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
----
[[oauth2Client-auth-grant-support]]
=== Authorization Grant Support
@@ -77,12 +77,12 @@ So long as this scheme is indicated, Resource Server will attempt to process the
Given a well-formed JWT, Resource Server will:
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header
2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWT
2. Validate the JWT's `exp` and `nbf` timestamps and the JWT's `iss` claim, and
3. Map each scope to an authority with the prefix `SCOPE_`.
[NOTE]
As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens.
As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate JWTs.
The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present.
@@ -456,9 +456,11 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
Converter<Jwt, AbstractAuthenticationToken> grantedAuthoritiesExtractor() {
JwtAuthenticationConverter jwtAuthenticationConverter =
new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
(new GrantedAuthoritiesExtractor());
return jwtAuthenticationConveter;
return jwtAuthenticationConverter;
}
----
+4 -4
View File
@@ -1,5 +1,5 @@
aspectjVersion=1.9.3
gaeVersion=1.9.76
springBootVersion=2.2.0.RELEASE
version=5.2.2.BUILD-SNAPSHOT
aspectjVersion=1.9.5
gaeVersion=1.9.81
springBootVersion=2.2.9.RELEASE
version=5.2.6.RELEASE
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
+55 -55
View File
@@ -1,16 +1,16 @@
if (!project.hasProperty('reactorVersion')) {
ext.reactorVersion = 'Dysprosium-SR1'
ext.reactorVersion = 'Dysprosium-SR10'
}
if (!project.hasProperty('springVersion')) {
ext.springVersion = '5.2.1.RELEASE'
ext.springVersion = '5.2.8.RELEASE'
}
if (!project.hasProperty('springDataVersion')) {
ext.springDataVersion = 'Moore-SR1'
ext.springDataVersion = 'Moore-SR9'
}
ext.rsocketVersion = '1.0.0-RC5'
ext.rsocketVersion = '1.0.1'
dependencyManagement {
imports {
@@ -20,17 +20,17 @@ dependencyManagement {
}
dependencies {
dependency 'cglib:cglib-nodep:3.3.0'
dependency 'com.squareup.okhttp3:mockwebserver:3.14.2'
dependency 'com.squareup.okhttp3:mockwebserver:3.14.9'
dependency 'opensymphony:sitemesh:2.4.2'
dependency 'org.gebish:geb-spock:0.10.0'
dependency 'org.jasig.cas:cas-server-webapp:4.2.7'
dependency 'org.powermock:powermock-api-mockito2:2.0.4'
dependency 'org.powermock:powermock-api-support:2.0.4'
dependency 'org.powermock:powermock-core:2.0.4'
dependency 'org.powermock:powermock-module-junit4-common:2.0.4'
dependency 'org.powermock:powermock-module-junit4:2.0.4'
dependency 'org.powermock:powermock-reflect:2.0.4'
dependency 'org.python:jython:2.5.0'
dependency 'org.powermock:powermock-api-mockito2:2.0.7'
dependency 'org.powermock:powermock-api-support:2.0.7'
dependency 'org.powermock:powermock-core:2.0.7'
dependency 'org.powermock:powermock-module-junit4-common:2.0.7'
dependency 'org.powermock:powermock-module-junit4:2.0.7'
dependency 'org.powermock:powermock-reflect:2.0.7'
dependency 'org.python:jython:2.5.3'
dependency 'org.spockframework:spock-core:1.0-groovy-2.4'
dependency 'org.spockframework:spock-spring:1.0-groovy-2.4'
}
@@ -42,9 +42,9 @@ dependencyManagement {
dependency 'asm:asm:3.1'
dependency 'ch.qos.logback:logback-classic:1.2.3'
dependency 'ch.qos.logback:logback-core:1.2.3'
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.10.0'
dependency 'com.fasterxml.jackson.core:jackson-core:2.10.0'
dependency 'com.fasterxml.jackson.core:jackson-databind:2.10.0'
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.10.5'
dependency 'com.fasterxml.jackson.core:jackson-core:2.10.5'
dependency 'com.fasterxml.jackson.core:jackson-databind:2.10.5'
dependency 'com.fasterxml:classmate:1.3.4'
dependency 'com.github.stephenc.jcip:jcip-annotations:1.0-1'
dependency 'com.google.appengine:appengine-api-1.0-sdk:$gaeVersion'
@@ -58,21 +58,21 @@ dependencyManagement {
dependency 'com.nimbusds:lang-tag:1.4.3'
dependency 'com.nimbusds:nimbus-jose-jwt:7.8.1'
dependency 'com.nimbusds:oauth2-oidc-sdk:6.14'
dependency 'com.squareup.okhttp3:okhttp:3.14.1'
dependency 'com.squareup.okhttp3:okhttp:3.14.9'
dependency 'com.squareup.okio:okio:1.13.0'
dependency 'com.sun.xml.bind:jaxb-core:2.3.0.1'
dependency 'com.sun.xml.bind:jaxb-impl:2.3.2'
dependency 'com.unboundid:unboundid-ldapsdk:4.0.12'
dependency 'com.sun.xml.bind:jaxb-impl:2.3.3'
dependency 'com.unboundid:unboundid-ldapsdk:4.0.14'
dependency 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1'
dependency 'commons-cli:commons-cli:1.4'
dependency 'commons-codec:commons-codec:1.13'
dependency 'commons-codec:commons-codec:1.14'
dependency 'commons-collections:commons-collections:3.2.2'
dependency 'commons-httpclient:commons-httpclient:3.1'
dependency 'commons-io:commons-io:2.6'
dependency 'commons-lang:commons-lang:2.6'
dependency 'commons-logging:commons-logging:1.2'
dependency 'dom4j:dom4j:1.6.1'
dependency 'io.projectreactor.tools:blockhound:1.0.1.RELEASE'
dependency 'io.projectreactor.tools:blockhound:1.0.4.RELEASE'
dependency "io.rsocket:rsocket-core:${rsocketVersion}"
dependency "io.rsocket:rsocket-transport-netty:${rsocketVersion}"
dependency 'javax.activation:activation:1.1.1'
@@ -86,8 +86,8 @@ dependencyManagement {
dependency 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'
dependency 'junit:junit:4.12'
dependency 'ldapsdk:ldapsdk:4.1'
dependency 'net.bytebuddy:byte-buddy-agent:1.9.10'
dependency 'net.bytebuddy:byte-buddy:1.9.10'
dependency 'net.bytebuddy:byte-buddy-agent:1.9.16'
dependency 'net.bytebuddy:byte-buddy:1.9.16'
dependency 'net.jcip:jcip-annotations:1.0'
dependency 'net.minidev:accessors-smart:1.2'
dependency 'net.minidev:json-smart:2.3'
@@ -138,19 +138,19 @@ dependencyManagement {
dependency 'org.apache.directory.shared:shared-cursor:0.9.15'
dependency 'org.apache.directory.shared:shared-ldap-constants:0.9.15'
dependency 'org.apache.directory.shared:shared-ldap:0.9.15'
dependency 'org.apache.httpcomponents:httpclient:4.5.10'
dependency 'org.apache.httpcomponents:httpclient:4.5.12'
dependency 'org.apache.httpcomponents:httpcore:4.4.8'
dependency 'org.apache.httpcomponents:httpmime:4.5.3'
dependency 'org.apache.mina:mina-core:2.0.0-M6'
dependency 'org.apache.taglibs:taglibs-standard-impl:1.2.5'
dependency 'org.apache.taglibs:taglibs-standard-jstlel:1.2.5'
dependency 'org.apache.taglibs:taglibs-standard-spec:1.2.5'
dependency 'org.apache.tomcat.embed:tomcat-embed-core:9.0.24'
dependency 'org.apache.tomcat.embed:tomcat-embed-el:9.0.24'
dependency 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.24'
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:9.0.24'
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.23'
dependency 'org.apache.tomcat:tomcat-annotations-api:9.0.24'
dependency 'org.apache.tomcat.embed:tomcat-embed-core:9.0.37'
dependency 'org.apache.tomcat.embed:tomcat-embed-el:9.0.37'
dependency 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.37'
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:9.0.37'
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.57'
dependency 'org.apache.tomcat:tomcat-annotations-api:9.0.37'
dependency "org.aspectj:aspectjrt:$aspectjVersion"
dependency "org.aspectj:aspectjtools:$aspectjVersion"
dependency "org.aspectj:aspectjweaver:$aspectjVersion"
@@ -158,21 +158,21 @@ dependencyManagement {
dependency 'org.attoparser:attoparser:2.0.4.RELEASE'
dependency 'org.bouncycastle:bcpkix-jdk15on:1.64'
dependency 'org.bouncycastle:bcprov-jdk15on:1.64'
dependency 'org.codehaus.groovy:groovy-all:2.4.17'
dependency 'org.codehaus.groovy:groovy-json:2.4.17'
dependency 'org.codehaus.groovy:groovy:2.4.17'
dependency 'org.codehaus.groovy:groovy-all:2.4.20'
dependency 'org.codehaus.groovy:groovy-json:2.4.20'
dependency 'org.codehaus.groovy:groovy:2.4.20'
dependency 'org.eclipse.jdt:ecj:3.12.3'
dependency 'org.eclipse.jetty.websocket:websocket-api:9.4.19.v20190610'
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.19.v20190610'
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-client:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-http:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-io:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-security:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-server:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-servlet:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-util:9.4.19.v20190610'
dependency 'org.eclipse.jetty:jetty-xml:9.4.19.v20190610'
dependency 'org.eclipse.jetty.websocket:websocket-api:9.4.27.v20200227'
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.27.v20200227'
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-client:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-http:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-io:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-security:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-server:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-servlet:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-util:9.4.27.v20200227'
dependency 'org.eclipse.jetty:jetty-xml:9.4.27.v20200227'
dependency 'org.eclipse.persistence:javax.persistence:2.2.1'
dependency 'org.gebish:geb-ast:0.10.0'
dependency 'org.gebish:geb-core:0.10.0'
@@ -181,9 +181,9 @@ dependencyManagement {
dependency 'org.hamcrest:hamcrest-core:1.3'
dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final'
dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
dependency 'org.hibernate:hibernate-core:5.2.17.Final'
dependency 'org.hibernate:hibernate-entitymanager:5.4.8.Final'
dependency 'org.hibernate:hibernate-validator:6.1.0.Final'
dependency 'org.hibernate:hibernate-core:5.2.18.Final'
dependency 'org.hibernate:hibernate-entitymanager:5.4.13.Final'
dependency 'org.hibernate:hibernate-validator:6.1.2.Final'
dependency 'org.hsqldb:hsqldb:2.5.0'
dependency 'org.jasig.cas.client:cas-client-core:3.5.1'
dependency 'org.javassist:javassist:3.22.0-CR2'
@@ -193,21 +193,21 @@ dependencyManagement {
dependency 'org.mockito:mockito-core:3.0.0'
dependency 'org.objenesis:objenesis:2.6'
dependency 'org.openid4java:openid4java-nodeps:0.9.6'
dependency 'org.opensaml:opensaml-core:3.4.3'
dependency 'org.opensaml:opensaml-saml-api:3.4.3'
dependency 'org.opensaml:opensaml-saml-impl:3.4.3'
dependency 'org.opensaml:opensaml-core:3.4.5'
dependency 'org.opensaml:opensaml-saml-api:3.4.5'
dependency 'org.opensaml:opensaml-saml-impl:3.4.5'
dependency 'org.ow2.asm:asm:6.2.1'
dependency 'org.reactivestreams:reactive-streams:1.0.1'
dependency 'org.reactivestreams:reactive-streams:1.0.3'
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.36.0'
dependency 'org.seleniumhq.selenium:selenium-java:3.141.59'
dependency 'org.seleniumhq.selenium:selenium-support:3.141.59'
dependency 'org.seleniumhq.selenium:selenium-api:3.141.59'
dependency 'org.skyscreamer:jsonassert:1.5.0'
dependency 'org.slf4j:jcl-over-slf4j:1.7.28'
dependency 'org.slf4j:jul-to-slf4j:1.7.28'
dependency 'org.slf4j:log4j-over-slf4j:1.7.28'
dependency 'org.slf4j:slf4j-api:1.7.28'
dependency 'org.slf4j:slf4j-nop:1.7.28'
dependency 'org.slf4j:jcl-over-slf4j:1.7.30'
dependency 'org.slf4j:jul-to-slf4j:1.7.30'
dependency 'org.slf4j:log4j-over-slf4j:1.7.30'
dependency 'org.slf4j:slf4j-api:1.7.30'
dependency 'org.slf4j:slf4j-nop:1.7.30'
dependency 'org.sonatype.sisu.inject:cglib:2.2.1-v20090111'
dependency 'org.springframework.ldap:spring-ldap-core:2.3.2.RELEASE'
dependency 'org.synchronoss.cloud:nio-multipart-parser:1.1.0'
@@ -28,7 +28,7 @@ public class BasicAuthenticationTests extends AbstractWebServerIntegrationTests
@Test
public void httpBasicWhenAuthenticationRequiredAndNotAuthenticatedThen401() throws Exception {
MockMvc mockMvc = createMockMvc("classpath:/spring/http-security-basic.xml", "classpath:/spring/in-memory-provider.xml", "classpath:/spring/testapp-servlet.xml");
mockMvc.perform(get("secure/index"))
mockMvc.perform(get("/secure/index"))
.andExpect(status().isUnauthorized());
}
@@ -47,7 +47,7 @@ public class ConcurrentSessionManagementTests extends AbstractWebServerIntegrati
MockMvc mockMvc = createMockMvc("classpath:/spring/http-security-concurrency.xml", "classpath:/spring/in-memory-provider.xml", "classpath:/spring/testapp-servlet.xml");
mockMvc.perform(get("secure/index").session(session1))
mockMvc.perform(get("/secure/index").session(session1))
.andExpect(status().is3xxRedirection());
MockHttpServletRequestBuilder login1 = login()
@@ -0,0 +1,144 @@
/*
* 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.ldap.server;
import org.junit.After;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.ContextSource;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
/**
* Tests for {@link UnboundIdContainer}, specifically relating to LDIF file detection.
*
* @author Eleftheria Stein
*/
public class UnboundIdContainerLdifTests {
AnnotationConfigApplicationContext appCtx;
@After
public void closeAppContext() {
if (appCtx != null) {
appCtx.close();
appCtx = null;
}
}
@Test
public void unboundIdContainerWhenCustomLdifNameThenLdifLoaded() {
appCtx = new AnnotationConfigApplicationContext(CustomLdifConfig.class);
DefaultSpringSecurityContextSource contextSource = (DefaultSpringSecurityContextSource) appCtx
.getBean(ContextSource.class);
SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(contextSource);
assertThat(template.compare("uid=bob,ou=people", "uid", "bob")).isTrue();
}
@Configuration
static class CustomLdifConfig {
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
"classpath:test-server.ldif");
@Bean
UnboundIdContainer ldapContainer() {
this.container.setPort(0);
return this.container;
}
@Bean
ContextSource contextSource(UnboundIdContainer container) {
return new DefaultSpringSecurityContextSource("ldap://127.0.0.1:"
+ container.getPort() + "/dc=springframework,dc=org");
}
@PreDestroy
void shutdown() {
this.container.stop();
}
}
@Test
public void unboundIdContainerWhenWildcardLdifNameThenLdifLoaded() {
appCtx = new AnnotationConfigApplicationContext(WildcardLdifConfig.class);
DefaultSpringSecurityContextSource contextSource = (DefaultSpringSecurityContextSource) appCtx
.getBean(ContextSource.class);
SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(contextSource);
assertThat(template.compare("uid=bob,ou=people", "uid", "bob")).isTrue();
}
@Configuration
static class WildcardLdifConfig {
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
"classpath*:test-server.ldif");
@Bean
UnboundIdContainer ldapContainer() {
this.container.setPort(0);
return this.container;
}
@Bean
ContextSource contextSource(UnboundIdContainer container) {
return new DefaultSpringSecurityContextSource("ldap://127.0.0.1:"
+ container.getPort() + "/dc=springframework,dc=org");
}
@PreDestroy
void shutdown() {
this.container.stop();
}
}
@Test
public void unboundIdContainerWhenMalformedLdifThenException() {
try {
appCtx = new AnnotationConfigApplicationContext(MalformedLdifConfig.class);
failBecauseExceptionWasNotThrown(IllegalStateException.class);
} catch (Exception e) {
assertThat(e.getCause()).isInstanceOf(IllegalStateException.class);
assertThat(e.getMessage()).contains("Unable to load LDIF classpath:test-server-malformed.txt");
}
}
@Configuration
static class MalformedLdifConfig {
private UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org",
"classpath:test-server-malformed.txt");
@Bean
UnboundIdContainer ldapContainer() {
this.container.setPort(0);
return this.container;
}
@PreDestroy
void shutdown() {
this.container.stop();
}
}
}
@@ -0,0 +1,9 @@
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups
dn ou=subgroups,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: subgroups
@@ -16,6 +16,7 @@
package org.springframework.security.ldap.authentication.ad;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.ldap.CommunicationException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.support.DefaultDirObjectFactory;
@@ -24,6 +25,7 @@ import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
@@ -142,12 +144,15 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
UsernamePasswordAuthenticationToken auth) {
String username = auth.getName();
String password = (String) auth.getCredentials();
DirContext ctx = bindAsUser(username, password);
DirContext ctx = null;
try {
ctx = bindAsUser(username, password);
return searchForUser(ctx, username);
}
catch (CommunicationException e) {
throw badLdapConnection(e);
}
catch (NamingException e) {
logger.error("Failed to locate directory entry for authenticated user: "
+ username, e);
@@ -210,8 +215,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
|| (e instanceof OperationNotSupportedException)) {
handleBindException(bindPrincipal, e);
throw badCredentials(e);
}
else {
} else {
throw LdapUtils.convertLdapException(e);
}
}
@@ -313,6 +317,12 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
return (BadCredentialsException) badCredentials().initCause(cause);
}
private InternalAuthenticationServiceException badLdapConnection(Throwable cause) {
return new InternalAuthenticationServiceException(messages.getMessage(
"LdapAuthenticationProvider.badLdapConnection",
"Connection to LDAP server failed."), cause);
}
private DirContextOperations searchForUser(DirContext context, String username)
throws NamingException {
SearchControls searchControls = new SearchControls();
@@ -327,6 +337,9 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends
searchControls, searchRoot, searchFilter,
new Object[] { bindPrincipal, username });
}
catch (CommunicationException ldapCommunicationException) {
throw badLdapConnection(ldapCommunicationException);
}
catch (IncorrectResultSizeDataAccessException incorrectResults) {
// Search should never return multiple results if properly configured - just
// rethrow
@@ -114,10 +114,10 @@ public class UnboundIdContainer implements InitializingBean, DisposableBean, Lif
private void importLdif(InMemoryDirectoryServer directoryServer) {
if (StringUtils.hasText(this.ldif)) {
Resource resource = this.context.getResource(this.ldif);
try {
if (resource.exists()) {
try (InputStream inputStream = resource.getInputStream()) {
Resource[] resources = this.context.getResources(this.ldif);
if (resources.length > 0 && resources[0].exists()) {
try (InputStream inputStream = resources[0].getInputStream()) {
directoryServer.importFromLDIF(false, new LDIFReader(inputStream));
}
}
@@ -32,6 +32,7 @@ import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
@@ -58,6 +59,9 @@ import static org.springframework.security.ldap.authentication.ad.ActiveDirector
* @author Rob Winch
*/
public class ActiveDirectoryLdapAuthenticationProviderTests {
public static final String EXISTING_LDAP_PROVIDER = "ldap://192.168.1.200/";
public static final String NON_EXISTING_LDAP_PROVIDER = "ldap://192.168.1.201/";
@Rule
public ExpectedException thrown = ExpectedException.none();
@@ -378,16 +382,29 @@ public class ActiveDirectoryLdapAuthenticationProviderTests {
}
@Test(expected = org.springframework.ldap.CommunicationException.class)
public void nonAuthenticationExceptionIsConvertedToSpringLdapException() {
provider.contextFactory = createContextFactoryThrowing(new CommunicationException(
msg));
provider.authenticate(joe);
public void nonAuthenticationExceptionIsConvertedToSpringLdapException() throws Throwable {
try {
provider.contextFactory = createContextFactoryThrowing(new CommunicationException(
msg));
provider.authenticate(joe);
} catch (InternalAuthenticationServiceException e) {
// Since GH-8418 ldap communication exception is wrapped into InternalAuthenticationServiceException.
// This test is about the wrapped exception, so we throw it.
throw e.getCause();
}
}
@Test(expected = org.springframework.security.authentication.InternalAuthenticationServiceException.class )
public void connectionExceptionIsWrappedInInternalException() throws Exception {
ActiveDirectoryLdapAuthenticationProvider noneReachableProvider = new ActiveDirectoryLdapAuthenticationProvider(
"mydomain.eu", NON_EXISTING_LDAP_PROVIDER, "dc=ad,dc=eu,dc=mydomain");
noneReachableProvider.doAuthentication(joe);
}
@Test
public void rootDnProvidedSeparatelyFromDomainAlsoWorks() throws Exception {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(
"mydomain.eu", "ldap://192.168.1.200/", "dc=ad,dc=eu,dc=mydomain");
"mydomain.eu", EXISTING_LDAP_PROVIDER, "dc=ad,dc=eu,dc=mydomain");
checkAuthentication("dc=ad,dc=eu,dc=mydomain", provider);
}
@@ -413,8 +430,11 @@ public class ActiveDirectoryLdapAuthenticationProviderTests {
provider.authenticate(joe);
fail("CommunicationException was expected with a root cause of ClassNotFoundException");
}
catch (org.springframework.ldap.CommunicationException expected) {
assertThat(expected.getRootCause()).isInstanceOf(ClassNotFoundException.class);
catch (InternalAuthenticationServiceException expected) {
assertThat(expected.getCause()).isInstanceOf(org.springframework.ldap.CommunicationException.class);
org.springframework.ldap.CommunicationException cause =
(org.springframework.ldap.CommunicationException) expected.getCause();
assertThat(cause.getRootCause()).isInstanceOf(ClassNotFoundException.class);
}
}
@@ -0,0 +1,137 @@
/*
* Copyright 2002-2019 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.client;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
/**
* An implementation of an {@link ReactiveOAuth2AuthorizedClientManager}
* that is capable of operating outside of a {@code ServerHttpRequest} context,
* e.g. in a scheduled/background thread and/or in the service-tier.
*
* <p>This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}</p>
*
* @author Ankur Pathak
* @author Phil Clay
* @see ReactiveOAuth2AuthorizedClientManager
* @see ReactiveOAuth2AuthorizedClientProvider
* @see ReactiveOAuth2AuthorizedClientService
* @since 5.2.2
*/
public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
implements ReactiveOAuth2AuthorizedClientManager {
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty();
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper = new DefaultContextAttributesMapper();
/**
* Constructs an {@code AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager} using the provided parameters.
*
* @param clientRegistrationRepository the repository of client registrations
* @param authorizedClientService the authorized client service
*/
public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientService = authorizedClientService;
}
@Override
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
return createAuthorizationContext(authorizeRequest)
.flatMap(this::authorizeAndSave);
}
private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2AuthorizeRequest authorizeRequest) {
String clientRegistrationId = authorizeRequest.getClientRegistrationId();
Authentication principal = authorizeRequest.getPrincipal();
return Mono.justOrEmpty(authorizeRequest.getAuthorizedClient())
.map(OAuth2AuthorizationContext::withAuthorizedClient)
.switchIfEmpty(Mono.defer(() -> this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
.flatMap(clientRegistration -> this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, principal.getName())
.map(OAuth2AuthorizationContext::withAuthorizedClient)
.switchIfEmpty(Mono.fromSupplier(() -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration))))
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("Could not find ClientRegistration with id '" + clientRegistrationId + "'")))))
.flatMap(contextBuilder -> this.contextAttributesMapper.apply(authorizeRequest)
.defaultIfEmpty(Collections.emptyMap())
.map(contextAttributes -> {
OAuth2AuthorizationContext.Builder builder = contextBuilder.principal(principal);
if (!contextAttributes.isEmpty()) {
builder = builder.attributes(attributes -> attributes.putAll(contextAttributes));
}
return builder.build();
}));
}
private Mono<OAuth2AuthorizedClient> authorizeAndSave(OAuth2AuthorizationContext authorizationContext) {
return this.authorizedClientProvider.authorize(authorizationContext)
.flatMap(authorizedClient -> this.authorizedClientService.saveAuthorizedClient(
authorizedClient,
authorizationContext.getPrincipal())
.thenReturn(authorizedClient))
.switchIfEmpty(Mono.defer(()-> Mono.justOrEmpty(authorizationContext.getAuthorizedClient())));
}
/**
* Sets the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client.
*
* @param authorizedClientProvider the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client
*/
public void setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
this.authorizedClientProvider = authorizedClientProvider;
}
/**
* Sets the {@code Function} used for mapping attribute(s) from the {@link OAuth2AuthorizeRequest} to a {@code Map} of attributes
* to be associated to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}.
*
* @param contextAttributesMapper the {@code Function} used for supplying the {@code Map} of attributes
* to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}
*/
public void setContextAttributesMapper(Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper) {
Assert.notNull(contextAttributesMapper, "contextAttributesMapper cannot be null");
this.contextAttributesMapper = contextAttributesMapper;
}
/**
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
*/
public static class DefaultContextAttributesMapper implements Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> {
private final AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper mapper =
new AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper();
@Override
public Mono<Map<String, Object>> apply(OAuth2AuthorizeRequest authorizeRequest) {
return Mono.fromCallable(() -> mapper.apply(authorizeRequest));
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -20,7 +20,11 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.util.Assert;
/**
@@ -40,6 +44,7 @@ import org.springframework.util.Assert;
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
*/
public class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
/**
@@ -59,8 +64,18 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
(OAuth2AuthorizationCodeAuthenticationToken) authentication;
OAuth2AuthorizationExchangeValidator.validate(
authorizationCodeAuthentication.getAuthorizationExchange());
OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication
.getAuthorizationExchange().getAuthorizationResponse();
if (authorizationResponse.statusError()) {
throw new OAuth2AuthorizationException(authorizationResponse.getError());
}
OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication
.getAuthorizationExchange().getAuthorizationRequest();
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
throw new OAuth2AuthorizationException(oauth2Error);
}
OAuth2AccessTokenResponse accessTokenResponse =
this.accessTokenResponseClient.getTokenResponse(
@@ -73,7 +88,8 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(),
accessTokenResponse.getAccessToken(),
accessTokenResponse.getRefreshToken());
accessTokenResponse.getRefreshToken(),
accessTokenResponse.getAdditionalParameters());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
return authenticationResult;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -22,9 +22,13 @@ import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessT
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
@@ -55,8 +59,8 @@ import java.util.function.Function;
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
*/
public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements
ReactiveAuthenticationManager {
public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements ReactiveAuthenticationManager {
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private final ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
public OAuth2AuthorizationCodeReactiveAuthenticationManager(
@@ -70,7 +74,16 @@ public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements
return Mono.defer(() -> {
OAuth2AuthorizationCodeAuthenticationToken token = (OAuth2AuthorizationCodeAuthenticationToken) authentication;
OAuth2AuthorizationExchangeValidator.validate(token.getAuthorizationExchange());
OAuth2AuthorizationResponse authorizationResponse = token.getAuthorizationExchange().getAuthorizationResponse();
if (authorizationResponse.statusError()) {
return Mono.error(new OAuth2AuthorizationException(authorizationResponse.getError()));
}
OAuth2AuthorizationRequest authorizationRequest = token.getAuthorizationExchange().getAuthorizationRequest();
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
return Mono.error(new OAuth2AuthorizationException(oauth2Error));
}
OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest(
token.getClientRegistration(),
@@ -1,53 +0,0 @@
/*
* Copyright 2002-2018 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.client.authentication;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
/**
* A validator for an &quot;exchange&quot; of an OAuth 2.0 Authorization Request and Response.
*
* @author Joe Grandja
* @since 5.1
* @see OAuth2AuthorizationExchange
*/
final class OAuth2AuthorizationExchangeValidator {
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
static void validate(OAuth2AuthorizationExchange authorizationExchange) {
OAuth2AuthorizationRequest authorizationRequest = authorizationExchange.getAuthorizationRequest();
OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse();
if (authorizationResponse.statusError()) {
throw new OAuth2AuthorizationException(authorizationResponse.getError());
}
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
throw new OAuth2AuthorizationException(oauth2Error);
}
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
throw new OAuth2AuthorizationException(oauth2Error);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -28,7 +28,6 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
@@ -60,7 +59,7 @@ import java.util.Map;
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
*/
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
@@ -74,59 +73,54 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
Assert.notNull(userService, "userService cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
this.authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient);
this.userService = userService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken authorizationCodeAuthentication =
OAuth2LoginAuthenticationToken loginAuthenticationToken =
(OAuth2LoginAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (authorizationCodeAuthentication.getAuthorizationExchange()
if (loginAuthenticationToken.getAuthorizationExchange()
.getAuthorizationRequest().getScopes().contains("openid")) {
// This is an OpenID Connect Authentication Request so return null
// and let OidcAuthorizationCodeAuthenticationProvider handle it instead
return null;
}
OAuth2AccessTokenResponse accessTokenResponse;
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
try {
OAuth2AuthorizationExchangeValidator.validate(
authorizationCodeAuthentication.getAuthorizationExchange());
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
new OAuth2AuthorizationCodeGrantRequest(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange()));
authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
loginAuthenticationToken.getClientRegistration(),
loginAuthenticationToken.getAuthorizationExchange()));
} catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters));
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities =
this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(),
loginAuthenticationToken.getClientRegistration(),
loginAuthenticationToken.getAuthorizationExchange(),
oauth2User,
mappedAuthorities,
accessToken,
accessTokenResponse.getRefreshToken());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
authorizationCodeAuthenticationToken.getRefreshToken());
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
return authenticationResult;
}
@@ -78,7 +78,6 @@ import java.util.Map;
*/
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
@@ -132,11 +131,6 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
OAuth2AccessTokenResponse accessTokenResponse;
try {
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -80,7 +80,6 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
ReactiveAuthenticationManager {
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
@@ -122,18 +121,14 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
.getAuthorizationExchange().getAuthorizationResponse();
if (authorizationResponse.statusError()) {
throw new OAuth2AuthenticationException(
authorizationResponse.getError(), authorizationResponse.getError().toString());
return Mono.error(new OAuth2AuthenticationException(
authorizationResponse.getError(), authorizationResponse.getError().toString()));
}
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
return Mono.error(new OAuth2AuthenticationException(
oauth2Error, oauth2Error.toString()));
}
OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest(
@@ -145,7 +140,7 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
.onErrorMap(OAuth2AuthorizationException.class, e -> new OAuth2AuthenticationException(e.getError(), e.getError().toString()))
.onErrorMap(JwtException.class, e -> {
OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, e.getMessage(), null);
throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), e);
return new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), e);
});
});
}
@@ -172,7 +167,7 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
INVALID_ID_TOKEN_ERROR_CODE,
"Missing (required) ID Token in Token Response for Client Registration: " + clientRegistration.getRegistrationId(),
null);
throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString());
return Mono.error(new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()));
}
return createOidcToken(clientRegistration, accessTokenResponse)
@@ -146,9 +146,12 @@ public final class ClientRegistrations {
RequestEntity<Void> request = RequestEntity.get(uri).build();
Map<String, Object> configuration = rest.exchange(request, typeReference).getBody();
OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse);
return withProviderConfiguration(metadata, issuer.toASCIIString())
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer.toASCIIString())
.jwkSetUri(metadata.getJWKSetURI().toASCIIString());
if (metadata.getUserInfoEndpointURI() != null) {
builder.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString());
}
return builder;
};
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -28,6 +28,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@@ -121,9 +122,9 @@ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2Authori
private static HttpServletRequest getHttpServletRequestOrDefault(Map<String, Object> attributes) {
HttpServletRequest servletRequest = (HttpServletRequest) attributes.get(HttpServletRequest.class.getName());
if (servletRequest == null) {
ServletRequestAttributes context = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (context != null) {
servletRequest = context.getRequest();
RequestAttributes context = RequestContextHolder.getRequestAttributes();
if (context instanceof ServletRequestAttributes) {
servletRequest = ((ServletRequestAttributes) context).getRequest();
}
}
return servletRequest;
@@ -132,9 +133,9 @@ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2Authori
private static HttpServletResponse getHttpServletResponseOrDefault(Map<String, Object> attributes) {
HttpServletResponse servletResponse = (HttpServletResponse) attributes.get(HttpServletResponse.class.getName());
if (servletResponse == null) {
ServletRequestAttributes context = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (context != null) {
servletResponse = context.getResponse();
RequestAttributes context = RequestContextHolder.getRequestAttributes();
if (context instanceof ServletRequestAttributes) {
servletResponse = ((ServletRequestAttributes) context).getResponse();
}
}
return servletResponse;
@@ -99,15 +99,16 @@ public final class DefaultReactiveOAuth2AuthorizedClientManager implements React
private Mono<OAuth2AuthorizedClient> loadAuthorizedClient(String clientRegistrationId, Authentication principal, ServerWebExchange serverWebExchange) {
return Mono.justOrEmpty(serverWebExchange)
.switchIfEmpty(Mono.defer(() -> currentServerWebExchange()))
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("serverWebExchange cannot be null")))
.flatMap(exchange -> this.authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, exchange));
}
private Mono<OAuth2AuthorizedClient> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, ServerWebExchange serverWebExchange) {
return Mono.justOrEmpty(serverWebExchange)
.switchIfEmpty(Mono.defer(() -> currentServerWebExchange()))
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("serverWebExchange cannot be null")))
.flatMap(exchange -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, exchange)
.thenReturn(authorizedClient))
.defaultIfEmpty(authorizedClient);
.thenReturn(authorizedClient));
}
private static Mono<ServerWebExchange> currentServerWebExchange() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -41,6 +41,7 @@ import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.FilterChain;
@@ -48,6 +49,11 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
@@ -132,24 +138,39 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (this.shouldProcessAuthorizationResponse(request)) {
this.processAuthorizationResponse(request, response);
if (matchesAuthorizationResponse(request)) {
processAuthorizationResponse(request, response);
return;
}
filterChain.doFilter(request, response);
}
private boolean shouldProcessAuthorizationResponse(HttpServletRequest request) {
private boolean matchesAuthorizationResponse(HttpServletRequest request) {
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
return false;
}
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
if (authorizationRequest == null) {
return false;
}
String requestUrl = UrlUtils.buildFullRequestUrl(request.getScheme(), request.getServerName(),
request.getServerPort(), request.getRequestURI(), null);
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
// Compare redirect_uri
UriComponents requestUri = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request)).build();
UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri()).build();
Set<Map.Entry<String, List<String>>> requestUriParameters = new LinkedHashSet<>(requestUri.getQueryParams().entrySet());
Set<Map.Entry<String, List<String>>> redirectUriParameters = new LinkedHashSet<>(redirectUri.getQueryParams().entrySet());
// Remove the additional request parameters (if any) from the authorization response (request)
// before doing an exact comparison with the authorizationRequest.getRedirectUri() parameters (if any)
requestUriParameters.retainAll(redirectUriParameters);
if (Objects.equals(requestUri.getScheme(), redirectUri.getScheme()) &&
Objects.equals(requestUri.getUserInfo(), redirectUri.getUserInfo()) &&
Objects.equals(requestUri.getHost(), redirectUri.getHost()) &&
Objects.equals(requestUri.getPort(), redirectUri.getPort()) &&
Objects.equals(requestUri.getPath(), redirectUri.getPath()) &&
Objects.equals(requestUriParameters.toString(), redirectUriParameters.toString())) {
return true;
}
return false;
@@ -165,10 +186,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replaceQuery(null)
.build()
.toUriString();
String redirectUri = UrlUtils.buildFullRequestUrl(request);
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
OAuth2AuthorizationCodeAuthenticationToken authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken(
@@ -183,7 +201,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
} catch (OAuth2AuthorizationException ex) {
OAuth2Error error = ex.getError();
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(authorizationResponse.getRedirectUri())
.fromUriString(authorizationRequest.getRedirectUri())
.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
if (!StringUtils.isEmpty(error.getDescription())) {
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
@@ -206,7 +224,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request, response);
String redirectUrl = authorizationResponse.getRedirectUri();
String redirectUrl = authorizationRequest.getRedirectUri();
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest != null) {
redirectUrl = savedRequest.getRedirectUrl();
@@ -22,6 +22,7 @@ import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
@@ -35,6 +36,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.UnAuthenticatedServerOAuth2AuthorizedClientRepository;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
@@ -124,6 +126,17 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
.clientCredentials()
.password()
.build();
// gh-7544
if (authorizedClientRepository instanceof UnAuthenticatedServerOAuth2AuthorizedClientRepository) {
UnAuthenticatedReactiveOAuth2AuthorizedClientManager unauthenticatedAuthorizedClientManager =
new UnAuthenticatedReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository,
(UnAuthenticatedServerOAuth2AuthorizedClientRepository) authorizedClientRepository);
unauthenticatedAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return unauthenticatedAuthorizedClientManager;
}
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
@@ -266,7 +279,11 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
.clientCredentials(this::updateClientCredentialsProvider)
.password(configurer -> configurer.clockSkew(this.accessTokenExpiresSkew))
.build();
((DefaultReactiveOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
if (this.authorizedClientManager instanceof UnAuthenticatedReactiveOAuth2AuthorizedClientManager) {
((UnAuthenticatedReactiveOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
} else {
((DefaultReactiveOAuth2AuthorizedClientManager) this.authorizedClientManager).setAuthorizedClientProvider(authorizedClientProvider);
}
}
private void updateClientCredentialsProvider(ReactiveOAuth2AuthorizedClientProviderBuilder.ClientCredentialsGrantBuilder builder) {
@@ -376,4 +393,52 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
.headers(headers -> headers.setBearerAuth(authorizedClient.getAccessToken().getTokenValue()))
.build();
}
private static class UnAuthenticatedReactiveOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager {
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
private final UnAuthenticatedServerOAuth2AuthorizedClientRepository authorizedClientRepository;
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
private UnAuthenticatedReactiveOAuth2AuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
UnAuthenticatedServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientRepository = authorizedClientRepository;
}
@Override
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
String clientRegistrationId = authorizeRequest.getClientRegistrationId();
Authentication principal = authorizeRequest.getPrincipal();
return Mono.justOrEmpty(authorizeRequest.getAuthorizedClient())
.switchIfEmpty(Mono.defer(() -> this.authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, null)))
.flatMap(authorizedClient -> {
// Re-authorize
return Mono.just(OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient).principal(principal).build())
.flatMap(this.authorizedClientProvider::authorize)
.flatMap(reauthorizedClient -> this.authorizedClientRepository.saveAuthorizedClient(reauthorizedClient, principal, null).thenReturn(reauthorizedClient))
// Default to the existing authorizedClient if the client was not re-authorized
.defaultIfEmpty(authorizeRequest.getAuthorizedClient() != null ?
authorizeRequest.getAuthorizedClient() : authorizedClient);
})
.switchIfEmpty(Mono.deferWithContext(context ->
// Authorize
this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(
"Could not find ClientRegistration with id '" + clientRegistrationId + "'")))
.flatMap(clientRegistration -> Mono.just(OAuth2AuthorizationContext.withClientRegistration(clientRegistration).principal(principal).build()))
.flatMap(this.authorizedClientProvider::authorize)
.flatMap(authorizedClient -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, null).thenReturn(authorizedClient))
.subscriberContext(context)
));
}
private void setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
this.authorizedClientProvider = authorizedClientProvider;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -36,6 +36,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.reactive.function.client.ClientRequest;
@@ -389,15 +390,11 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
attrs.containsKey(HTTP_SERVLET_RESPONSE_ATTR_NAME)) {
return;
}
ServletRequestAttributes context = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = null;
HttpServletResponse response = null;
if (context != null) {
request = context.getRequest();
response = context.getResponse();
RequestAttributes context = RequestContextHolder.getRequestAttributes();
if (context instanceof ServletRequestAttributes) {
attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, ((ServletRequestAttributes) context).getRequest());
attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, ((ServletRequestAttributes) context).getResponse());
}
attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, request);
attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, response);
}
private void populateDefaultAuthentication(Map<String, Object> attrs) {
@@ -137,13 +137,11 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth
.switchIfEmpty(currentServerWebExchange());
return Mono.zip(defaultedRegistrationId, defaultedAuthentication, defaultedExchange)
.map(t3 -> {
OAuth2AuthorizeRequest.Builder builder = OAuth2AuthorizeRequest.withClientRegistrationId(t3.getT1()).principal(t3.getT2());
if (t3.getT3() != null) {
builder.attribute(ServerWebExchange.class.getName(), t3.getT3());
}
return builder.build();
});
.map(t3 -> OAuth2AuthorizeRequest.withClientRegistrationId(t3.getT1())
.principal(t3.getT2())
.attribute(ServerWebExchange.class.getName(), t3.getT3())
.build()
);
}
private Mono<Authentication> currentAuthentication() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -27,6 +27,8 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
@@ -37,13 +39,20 @@ import org.springframework.security.web.server.authentication.ServerAuthenticati
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
* which handles the processing of the OAuth 2.0 Authorization Response.
@@ -165,16 +174,22 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return this.requiresAuthenticationMatcher.matches(exchange)
.filter( matchResult -> matchResult.isMatch())
.flatMap( matchResult -> this.authenticationConverter.convert(exchange))
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.flatMap(matchResult ->
this.authenticationConverter.convert(exchange)
.onErrorMap(OAuth2AuthorizationException.class, e -> new OAuth2AuthenticationException(
e.getError(), e.getError().toString())))
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.flatMap( token -> authenticate(exchange, chain, token));
.flatMap(token -> authenticate(exchange, chain, token))
.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler
.onAuthenticationFailure(new WebFilterExchange(exchange, chain), e));
}
private Mono<Void> authenticate(ServerWebExchange exchange,
WebFilterChain chain, Authentication token) {
private Mono<Void> authenticate(ServerWebExchange exchange, WebFilterChain chain, Authentication token) {
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
return this.authenticationManager.authenticate(token)
.onErrorMap(OAuth2AuthorizationException.class, e -> new OAuth2AuthenticationException(
e.getError(), e.getError().toString()))
.switchIfEmpty(Mono.defer(() -> Mono.error(new IllegalStateException("No provider found for " + token.getClass()))))
.flatMap(authentication -> onAuthenticationSuccess(authentication, webFilterExchange))
.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler
@@ -198,20 +213,34 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
}
private Mono<ServerWebExchangeMatcher.MatchResult> matchesAuthorizationResponse(ServerWebExchange exchange) {
return this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
.flatMap(authorizationRequest -> {
String requestUrl = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
.query(null)
.build()
.toUriString();
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(queryParams)) {
return ServerWebExchangeMatcher.MatchResult.match();
}
return ServerWebExchangeMatcher.MatchResult.notMatch();
})
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
return Mono.just(exchange)
.filter(exch -> OAuth2AuthorizationResponseUtils.isAuthorizationResponse(exch.getRequest().getQueryParams()))
.flatMap(exch -> this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
.flatMap(authorizationRequest ->
matchesRedirectUri(exch.getRequest().getURI(), authorizationRequest.getRedirectUri())))
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
}
private static Mono<ServerWebExchangeMatcher.MatchResult> matchesRedirectUri(
URI authorizationResponseUri, String authorizationRequestRedirectUri) {
UriComponents requestUri = UriComponentsBuilder.fromUri(authorizationResponseUri).build();
UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequestRedirectUri).build();
Set<Map.Entry<String, List<String>>> requestUriParameters =
new LinkedHashSet<>(requestUri.getQueryParams().entrySet());
Set<Map.Entry<String, List<String>>> redirectUriParameters =
new LinkedHashSet<>(redirectUri.getQueryParams().entrySet());
// Remove the additional request parameters (if any) from the authorization response (request)
// before doing an exact comparison with the authorizationRequest.getRedirectUri() parameters (if any)
requestUriParameters.retainAll(redirectUriParameters);
if (Objects.equals(requestUri.getScheme(), redirectUri.getScheme()) &&
Objects.equals(requestUri.getUserInfo(), redirectUri.getUserInfo()) &&
Objects.equals(requestUri.getHost(), redirectUri.getHost()) &&
Objects.equals(requestUri.getPort(), redirectUri.getPort()) &&
Objects.equals(requestUri.getPath(), redirectUri.getPath()) &&
Objects.equals(requestUriParameters.toString(), redirectUriParameters.toString())) {
return ServerWebExchangeMatcher.MatchResult.match();
}
return ServerWebExchangeMatcher.MatchResult.notMatch();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -18,7 +18,6 @@ package org.springframework.security.oauth2.client.web.server;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
@@ -28,13 +27,12 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
/**
* Converts from a {@link ServerWebExchange} to an {@link OAuth2LoginAuthenticationToken} that can be authenticated. The
* Converts from a {@link ServerWebExchange} to an {@link OAuth2AuthorizationCodeAuthenticationToken} that can be authenticated. The
* converter does not validate any errors it only performs a conversion.
* @author Rob Winch
* @since 5.1
@@ -103,14 +101,10 @@ public class ServerOAuth2AuthorizationCodeAuthenticationTokenConverter
}
private static OAuth2AuthorizationResponse convertResponse(ServerWebExchange exchange) {
MultiValueMap<String, String> queryParams = exchange.getRequest()
.getQueryParams();
String redirectUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
.query(null)
.build()
.toUriString();
return OAuth2AuthorizationResponseUtils
.convert(queryParams, redirectUri);
.convert(exchange.getRequest().getQueryParams(), redirectUri);
}
}
@@ -0,0 +1,317 @@
/*
* Copyright 2002-2019 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.client;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.test.publisher.PublisherProbe;
import java.util.Map;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager}.
*
* @author Ankur Pathak
* @author Phil Clay
*/
public class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManagerTests {
private ReactiveClientRegistrationRepository clientRegistrationRepository;
private ReactiveOAuth2AuthorizedClientService authorizedClientService;
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider;
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper;
private AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager;
private ClientRegistration clientRegistration;
private Authentication principal;
private OAuth2AuthorizedClient authorizedClient;
private ArgumentCaptor<OAuth2AuthorizationContext> authorizationContextCaptor;
private PublisherProbe<Void> saveAuthorizedClientProbe;
@SuppressWarnings("unchecked")
@Before
public void setup() {
this.clientRegistrationRepository = mock(ReactiveClientRegistrationRepository.class);
this.authorizedClientService = mock(ReactiveOAuth2AuthorizedClientService.class);
this.saveAuthorizedClientProbe = PublisherProbe.empty();
when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn(this.saveAuthorizedClientProbe.mono());
this.authorizedClientProvider = mock(ReactiveOAuth2AuthorizedClientProvider.class);
this.contextAttributesMapper = mock(Function.class);
when(this.contextAttributesMapper.apply(any())).thenReturn(Mono.empty());
this.authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
this.clientRegistrationRepository, this.authorizedClientService);
this.authorizedClientManager.setAuthorizedClientProvider(this.authorizedClientProvider);
this.authorizedClientManager.setContextAttributesMapper(this.contextAttributesMapper);
this.clientRegistration = TestClientRegistrations.clientRegistration().build();
this.principal = new TestingAuthenticationToken("principal", "password");
this.authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, this.principal.getName(),
TestOAuth2AccessTokens.scopes("read", "write"), TestOAuth2RefreshTokens.refreshToken());
this.authorizationContextCaptor = ArgumentCaptor.forClass(OAuth2AuthorizationContext.class);
}
@Test
public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(null, this.authorizedClientService))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("clientRegistrationRepository cannot be null");
}
@Test
public void constructorWhenOAuth2AuthorizedClientServiceIsNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(this.clientRegistrationRepository, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizedClientService cannot be null");
}
@Test
public void setAuthorizedClientProviderWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.authorizedClientManager.setAuthorizedClientProvider(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizedClientProvider cannot be null");
}
@Test
public void setContextAttributesMapperWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.authorizedClientManager.setContextAttributesMapper(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("contextAttributesMapper cannot be null");
}
@Test
public void authorizeWhenRequestIsNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.authorizedClientManager.authorize(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizeRequest cannot be null");
}
@Test
public void authorizeWhenClientRegistrationNotFoundThenThrowIllegalArgumentException() {
String clientRegistrationId = "invalid-registration-id";
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId)
.principal(this.principal)
.build();
when(this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)).thenReturn(Mono.empty());
StepVerifier.create(this.authorizedClientManager.authorize(authorizeRequest))
.verifyError(IllegalArgumentException.class);
}
@SuppressWarnings("unchecked")
@Test
public void authorizeWhenNotAuthorizedAndUnsupportedProviderThenNotAuthorized() {
when(this.clientRegistrationRepository.findByRegistrationId(
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
when(this.authorizedClientService.loadAuthorizedClient(
any(), any())).thenReturn(Mono.empty());
when(authorizedClientProvider.authorize(any())).thenReturn(Mono.empty());
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
.principal(this.principal)
.build();
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
StepVerifier.create(authorizedClient).verifyComplete();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isNull();
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
verify(this.authorizedClientService, never()).saveAuthorizedClient(
any(OAuth2AuthorizedClient.class), eq(this.principal));
}
@SuppressWarnings("unchecked")
@Test
public void authorizeWhenNotAuthorizedAndSupportedProviderThenAuthorized() {
when(this.clientRegistrationRepository.findByRegistrationId(
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
when(this.authorizedClientService.loadAuthorizedClient(
any(), any())).thenReturn(Mono.empty());
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(this.authorizedClient));
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
.principal(this.principal)
.build();
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
StepVerifier.create(authorizedClient)
.expectNext(this.authorizedClient)
.verifyComplete();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isNull();
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
verify(this.authorizedClientService).saveAuthorizedClient(
eq(this.authorizedClient), eq(this.principal));
this.saveAuthorizedClientProbe.assertWasSubscribed();
}
@SuppressWarnings("unchecked")
@Test
public void authorizeWhenAuthorizedAndSupportedProviderThenReauthorized() {
when(this.clientRegistrationRepository.findByRegistrationId(
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
when(this.authorizedClientService.loadAuthorizedClient(
eq(this.clientRegistration.getRegistrationId()), eq(this.principal.getName()))).thenReturn(Mono.just(this.authorizedClient));
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
this.clientRegistration, this.principal.getName(),
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
.principal(this.principal)
.build();
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
StepVerifier.create(authorizedClient)
.expectNext(reauthorizedClient)
.verifyComplete();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
verify(this.authorizedClientService).saveAuthorizedClient(
eq(reauthorizedClient), eq(this.principal));
this.saveAuthorizedClientProbe.assertWasSubscribed();
}
@SuppressWarnings("unchecked")
@Test
public void reauthorizeWhenUnsupportedProviderThenNotReauthorized() {
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.empty());
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
.principal(this.principal)
.build();
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
StepVerifier.create(authorizedClient)
.expectNext(this.authorizedClient)
.verifyComplete();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
verify(this.contextAttributesMapper).apply(eq(reauthorizeRequest));
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
verify(this.authorizedClientService, never()).saveAuthorizedClient(
any(OAuth2AuthorizedClient.class), eq(this.principal));
}
@SuppressWarnings("unchecked")
@Test
public void reauthorizeWhenSupportedProviderThenReauthorized() {
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
this.clientRegistration, this.principal.getName(),
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
.principal(this.principal)
.build();
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
StepVerifier.create(authorizedClient)
.expectNext(reauthorizedClient)
.verifyComplete();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
verify(this.contextAttributesMapper).apply(eq(reauthorizeRequest));
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
verify(this.authorizedClientService).saveAuthorizedClient(
eq(reauthorizedClient), eq(this.principal));
this.saveAuthorizedClientProbe.assertWasSubscribed();
}
@SuppressWarnings("unchecked")
@Test
public void reauthorizeWhenRequestAttributeScopeThenMappedToContext() {
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
this.clientRegistration, this.principal.getName(),
TestOAuth2AccessTokens.noScopes(), TestOAuth2RefreshTokens.refreshToken());
when(this.authorizedClientProvider.authorize(any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(reauthorizedClient));
OAuth2AuthorizeRequest reauthorizeRequest = OAuth2AuthorizeRequest.withAuthorizedClient(this.authorizedClient)
.principal(this.principal)
.attribute(OAuth2ParameterNames.SCOPE, "read write")
.build();
this.authorizedClientManager.setContextAttributesMapper(new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.DefaultContextAttributesMapper());
Mono<OAuth2AuthorizedClient> authorizedClient = this.authorizedClientManager.authorize(reauthorizeRequest);
StepVerifier.create(authorizedClient)
.expectNext(reauthorizedClient)
.verifyComplete();
verify(this.authorizedClientService).saveAuthorizedClient(
eq(reauthorizedClient), eq(this.principal));
this.saveAuthorizedClientProbe.assertWasSubscribed();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isSameAs(this.authorizedClient);
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
assertThat(authorizationContext.getAttributes()).containsKey(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
String[] requestScopeAttribute = authorizationContext.getAttribute(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
assertThat(requestScopeAttribute).contains("read", "write");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -16,6 +16,8 @@
package org.springframework.security.oauth2.client.authentication;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
@@ -98,19 +100,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_state_parameter");
}
@Test
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthorizationException() {
OAuth2AuthorizationResponse authorizationResponse = success().redirectUri("https://example2.com").build();
OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(
this.authorizationRequest, authorizationResponse);
assertThatThrownBy(() -> {
this.authenticationProvider.authenticate(
new OAuth2AuthorizationCodeAuthenticationToken(
this.clientRegistration, authorizationExchange));
}).isInstanceOf(OAuth2AuthorizationException.class).hasMessageContaining("invalid_redirect_uri_parameter");
}
@Test
public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessToken() {
OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().refreshToken("refresh").build();
@@ -132,4 +121,26 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
assertThat(authenticationResult.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
assertThat(authenticationResult.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken());
}
// gh-5368
@Test
public void authenticateWhenAuthorizationSuccessResponseThenAdditionalParametersIncluded() {
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put("param1", "value1");
additionalParameters.put("param2", "value2");
OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().additionalParameters(additionalParameters)
.build();
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest,
success().build());
OAuth2AuthorizationCodeAuthenticationToken authentication = (OAuth2AuthorizationCodeAuthenticationToken) this.authenticationProvider
.authenticate(
new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, authorizationExchange));
assertThat(authentication.getAdditionalParameters())
.containsAllEntriesOf(accessTokenResponse.getAdditionalParameters());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -81,13 +81,6 @@ public class OAuth2AuthorizationCodeReactiveAuthenticationManagerTests {
.isInstanceOf(OAuth2AuthorizationException.class);
}
@Test
public void authenticateWhenRedirectUriNotEqualThenOAuth2AuthorizationException() {
this.authorizationRequest.redirectUri("https://example.org/notequal");
assertThatCode(() -> authenticate())
.isInstanceOf(OAuth2AuthorizationException.class);
}
@Test
public void authenticateWhenValidThenSuccess() {
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(this.tokenResponse.build()));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -151,20 +151,6 @@ public class OAuth2LoginAuthenticationProviderTests {
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
}
@Test
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
this.exception.expect(OAuth2AuthenticationException.class);
this.exception.expectMessage(containsString("invalid_redirect_uri_parameter"));
OAuth2AuthorizationResponse authorizationResponse =
success().redirectUri("https://example2.com").build();
OAuth2AuthorizationExchange authorizationExchange =
new OAuth2AuthorizationExchange(this.authorizationRequest, authorizationResponse);
this.authenticationProvider.authenticate(
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
}
@Test
public void authenticateWhenLoginSuccessThenReturnAuthentication() {
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse();
@@ -186,19 +186,6 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
}
@Test
public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
this.exception.expect(OAuth2AuthenticationException.class);
this.exception.expectMessage(containsString("invalid_redirect_uri_parameter"));
OAuth2AuthorizationResponse authorizationResponse = success().redirectUri("https://example2.com").build();
OAuth2AuthorizationExchange authorizationExchange =
new OAuth2AuthorizationExchange(this.authorizationRequest, authorizationResponse);
this.authenticationProvider.authenticate(
new OAuth2LoginAuthenticationToken(this.clientRegistration, authorizationExchange));
}
@Test
public void authenticateWhenTokenResponseDoesNotContainIdTokenThenThrowOAuth2AuthenticationException() {
this.exception.expect(OAuth2AuthenticationException.class);
@@ -195,6 +195,14 @@ public class ClientRegistrationsTest {
assertThat(provider.getJwkSetUri()).isNull();
}
// gh-8187
@Test
public void issuerWhenResponseMissingUserInfoUriThenSuccess() throws Exception {
this.response.remove("userinfo_endpoint");
ClientRegistration registration = registration("").build();
assertThat(registration.getProviderDetails().getUserInfoEndpoint().getUri()).isNull();
}
@Test
public void issuerWhenContainsTrailingSlashThenSuccess() throws Exception {
assertThat(registration("")).isNotNull();
@@ -65,6 +65,7 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
private MockServerWebExchange serverWebExchange;
private Context context;
private ArgumentCaptor<OAuth2AuthorizationContext> authorizationContextCaptor;
private PublisherProbe<OAuth2AuthorizedClient> loadAuthorizedClientProbe;
private PublisherProbe<Void> saveAuthorizedClientProbe;
@SuppressWarnings("unchecked")
@@ -74,8 +75,9 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
when(this.clientRegistrationRepository.findByRegistrationId(
anyString())).thenReturn(Mono.empty());
this.authorizedClientRepository = mock(ServerOAuth2AuthorizedClientRepository.class);
this.loadAuthorizedClientProbe = PublisherProbe.empty();
when(this.authorizedClientRepository.loadAuthorizedClient(
anyString(), any(Authentication.class), any(ServerWebExchange.class))).thenReturn(Mono.empty());
anyString(), any(Authentication.class), any(ServerWebExchange.class))).thenReturn(this.loadAuthorizedClientProbe.mono());
this.saveAuthorizedClientProbe = PublisherProbe.empty();
when(this.authorizedClientRepository.saveAuthorizedClient(
any(OAuth2AuthorizedClient.class), any(Authentication.class), any(ServerWebExchange.class))).thenReturn(this.saveAuthorizedClientProbe.mono());
@@ -131,6 +133,16 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
.hasMessage("authorizeRequest cannot be null");
}
@Test
public void authorizeWhenExchangeIsNullThenThrowIllegalArgumentException() {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
.principal(this.principal)
.build();
assertThatThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest).block())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("serverWebExchange cannot be null");
}
@Test
public void authorizeWhenClientRegistrationNotFoundThenThrowIllegalArgumentException() {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("invalid-registration-id")
@@ -162,7 +174,8 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
assertThat(authorizedClient).isNull();
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), any(), any());
this.loadAuthorizedClientProbe.assertWasSubscribed();
this.saveAuthorizedClientProbe.assertWasNotSubscribed();
}
@SuppressWarnings("unchecked")
@@ -193,38 +206,14 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
this.saveAuthorizedClientProbe.assertWasSubscribed();
}
@Test
public void authorizeWhenNotAuthorizedAndSupportedProviderAndExchangeUnavailableThenAuthorizedButNotSaved() {
when(this.clientRegistrationRepository.findByRegistrationId(
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
when(this.authorizedClientProvider.authorize(
any(OAuth2AuthorizationContext.class))).thenReturn(Mono.just(this.authorizedClient));
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(this.clientRegistration.getRegistrationId())
.principal(this.principal)
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest).block();
verify(this.authorizedClientProvider).authorize(this.authorizationContextCaptor.capture());
verify(this.contextAttributesMapper).apply(eq(authorizeRequest));
OAuth2AuthorizationContext authorizationContext = this.authorizationContextCaptor.getValue();
assertThat(authorizationContext.getClientRegistration()).isEqualTo(this.clientRegistration);
assertThat(authorizationContext.getAuthorizedClient()).isNull();
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
assertThat(authorizedClient).isSameAs(this.authorizedClient);
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), any(), any());
}
@SuppressWarnings("unchecked")
@Test
public void authorizeWhenAuthorizedAndSupportedProviderThenReauthorized() {
when(this.clientRegistrationRepository.findByRegistrationId(
eq(this.clientRegistration.getRegistrationId()))).thenReturn(Mono.just(this.clientRegistration));
this.loadAuthorizedClientProbe = PublisherProbe.of(Mono.just(this.authorizedClient));
when(this.authorizedClientRepository.loadAuthorizedClient(
eq(this.clientRegistration.getRegistrationId()), eq(this.principal), eq(this.serverWebExchange))).thenReturn(Mono.just(this.authorizedClient));
eq(this.clientRegistration.getRegistrationId()), eq(this.principal), eq(this.serverWebExchange))).thenReturn(this.loadAuthorizedClientProbe.mono());
OAuth2AuthorizedClient reauthorizedClient = new OAuth2AuthorizedClient(
this.clientRegistration, this.principal.getName(),
@@ -313,7 +302,7 @@ public class DefaultReactiveOAuth2AuthorizedClientManagerTests {
assertThat(authorizationContext.getPrincipal()).isEqualTo(this.principal);
assertThat(authorizedClient).isSameAs(this.authorizedClient);
verify(this.authorizedClientRepository, never()).saveAuthorizedClient(any(), any(), any());
this.saveAuthorizedClientProbe.assertWasNotSubscribed();
}
@SuppressWarnings("unchecked")
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -15,17 +15,9 @@
*/
package org.springframework.security.oauth2.client.web;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
@@ -50,13 +42,26 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.CollectionUtils;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.core.TestOAuth2AccessTokens.noScopes;
import static org.springframework.security.oauth2.core.TestOAuth2RefreshTokens.refreshToken;
@@ -131,8 +136,7 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
// NOTE: A valid Authorization Response contains either a 'code' or 'error' parameter.
HttpServletResponse response = mock(HttpServletResponse.class);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
@@ -142,94 +146,142 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
@Test
public void doFilterWhenAuthorizationRequestNotFoundThenNotProcessed() throws Exception {
String requestUri = "/path";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationResponseUrlDoesNotMatchAuthorizationRequestRedirectUriThenNotProcessed() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
request.setRequestURI(requestUri + "-no-match");
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationResponseValidThenAuthorizationRequestRemoved() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/path");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationRequestRedirectUriDoesNotMatchThenNotProcessed() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri);
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
authorizationResponse.setRequestURI(requestUri + "-no-match");
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
// gh-7963
@Test
public void doFilterWhenAuthorizationRequestRedirectUriParametersMatchThenProcessed() throws Exception {
// 1) redirect_uri with query parameters
String requestUri = "/callback/client-1";
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri, parameters);
MockHttpServletResponse response = new MockHttpServletResponse();
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
FilterChain filterChain = mock(FilterChain.class);
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
this.filter.doFilter(authorizationResponse, response, filterChain);
verifyZeroInteractions(filterChain);
// 2) redirect_uri with query parameters AND authorization response additional parameters
Map<String, String> additionalParameters = new LinkedHashMap<>();
additionalParameters.put("auth-param1", "value1");
additionalParameters.put("auth-param2", "value2");
response = new MockHttpServletResponse();
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
authorizationResponse = createAuthorizationResponse(authorizationRequest, additionalParameters);
this.filter.doFilter(authorizationResponse, response, filterChain);
verifyZeroInteractions(filterChain);
}
// gh-7963
@Test
public void doFilterWhenAuthorizationRequestRedirectUriParametersDoesNotMatchThenNotProcessed() throws Exception {
String requestUri = "/callback/client-1";
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri, parameters);
MockHttpServletResponse response = new MockHttpServletResponse();
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
FilterChain filterChain = mock(FilterChain.class);
// 1) Parameter value
Map<String, String> parametersNotMatch = new LinkedHashMap<>(parameters);
parametersNotMatch.put("param2", "value8");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
authorizationResponse.setSession(authorizationRequest.getSession());
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
// 2) Parameter order
parametersNotMatch = new LinkedHashMap<>();
parametersNotMatch.put("param2", "value2");
parametersNotMatch.put("param1", "value1");
authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
authorizationResponse.setSession(authorizationRequest.getSession());
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain, times(2)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
// 3) Parameter missing
parametersNotMatch = new LinkedHashMap<>(parameters);
parametersNotMatch.remove("param2");
authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
authorizationResponse.setSession(authorizationRequest.getSession());
this.filter.doFilter(authorizationResponse, response, filterChain);
verify(filterChain, times(3)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenAuthorizationRequestMatchThenAuthorizationRequestRemoved() throws Exception {
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain);
this.filter.doFilter(authorizationResponse, response, filterChain);
assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(request)).isNull();
assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(authorizationResponse)).isNull();
}
@Test
public void doFilterWhenAuthorizationFailsThenHandleOAuth2AuthorizationException() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthorizationRequest(request, response, this.registration1);
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT);
when(this.authenticationManager.authenticate(any(Authentication.class)))
.thenThrow(new OAuth2AuthorizationException(error));
this.filter.doFilter(request, response, filterChain);
this.filter.doFilter(authorizationResponse, response, filterChain);
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1?error=invalid_grant");
}
@Test
public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSavedToService() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
public void doFilterWhenAuthorizationSucceedsThenAuthorizedClientSavedToService() throws Exception {
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain);
this.filter.doFilter(authorizationResponse, response, filterChain);
OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
this.registration1.getRegistrationId(), this.principalName1);
@@ -241,40 +293,31 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
}
@Test
public void doFilterWhenAuthorizationResponseSuccessThenRedirected() throws Exception {
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
public void doFilterWhenAuthorizationSucceedsThenRedirected() throws Exception {
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain);
this.filter.doFilter(authorizationResponse, response, filterChain);
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1");
}
@Test
public void doFilterWhenAuthorizationResponseSuccessHasSavedRequestThenRedirectedToSavedRequest() throws Exception {
public void doFilterWhenAuthorizationSucceedsAndHasSavedRequestThenRedirectToSavedRequest() throws Exception {
String requestUri = "/saved-request";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
RequestCache requestCache = new HttpSessionRequestCache();
requestCache.saveRequest(request, response);
requestUri = "/callback/client-1";
request.setRequestURI(requestUri);
request.setRequestURI("/callback/client-1");
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
@@ -284,36 +327,30 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
}
@Test
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception {
public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception {
AnonymousAuthenticationToken anonymousPrincipal =
new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(anonymousPrincipal);
SecurityContextHolder.setContext(securityContext);
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain);
this.filter.doFilter(authorizationResponse, response, filterChain);
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
this.registration1.getRegistrationId(), anonymousPrincipal, request);
this.registration1.getRegistrationId(), anonymousPrincipal, authorizationResponse);
assertThat(authorizedClient).isNotNull();
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
assertThat(authorizedClient.getPrincipalName()).isEqualTo(anonymousPrincipal.getName());
assertThat(authorizedClient.getAccessToken()).isNotNull();
HttpSession session = request.getSession(false);
HttpSession session = authorizationResponse.getSession(false);
assertThat(session).isNotNull();
@SuppressWarnings("unchecked")
@@ -325,33 +362,27 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
}
@Test
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception {
public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception {
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(securityContext); // null Authentication
String requestUri = "/callback/client-1";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
request.addParameter(OAuth2ParameterNames.CODE, "code");
request.addParameter(OAuth2ParameterNames.STATE, "state");
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.setUpAuthorizationRequest(request, response, this.registration1);
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
this.setUpAuthenticationResult(this.registration1);
this.filter.doFilter(request, response, filterChain);
this.filter.doFilter(authorizationResponse, response, filterChain);
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
this.registration1.getRegistrationId(), null, request);
this.registration1.getRegistrationId(), null, authorizationResponse);
assertThat(authorizedClient).isNotNull();
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
assertThat(authorizedClient.getPrincipalName()).isEqualTo("anonymousUser");
assertThat(authorizedClient.getAccessToken()).isNotNull();
HttpSession session = request.getSession(false);
HttpSession session = authorizationResponse.getSession(false);
assertThat(session).isNotNull();
@SuppressWarnings("unchecked")
@@ -362,13 +393,51 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient);
}
private static MockHttpServletRequest createAuthorizationRequest(String requestUri) {
return createAuthorizationRequest(requestUri, new LinkedHashMap<>());
}
private static MockHttpServletRequest createAuthorizationRequest(String requestUri, Map<String, String> parameters) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
if (!CollectionUtils.isEmpty(parameters)) {
parameters.forEach(request::addParameter);
request.setQueryString(
parameters.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&")));
}
return request;
}
private static MockHttpServletRequest createAuthorizationResponse(MockHttpServletRequest authorizationRequest) {
return createAuthorizationResponse(authorizationRequest, new LinkedHashMap<>());
}
private static MockHttpServletRequest createAuthorizationResponse(
MockHttpServletRequest authorizationRequest, Map<String, String> additionalParameters) {
MockHttpServletRequest authorizationResponse = new MockHttpServletRequest(
authorizationRequest.getMethod(), authorizationRequest.getRequestURI());
authorizationResponse.setServletPath(authorizationRequest.getRequestURI());
authorizationRequest.getParameterMap().forEach(authorizationResponse::addParameter);
authorizationResponse.addParameter(OAuth2ParameterNames.CODE, "code");
authorizationResponse.addParameter(OAuth2ParameterNames.STATE, "state");
additionalParameters.forEach(authorizationResponse::addParameter);
authorizationResponse.setQueryString(
authorizationResponse.getParameterMap().entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue()[0])
.collect(Collectors.joining("&")));
authorizationResponse.setSession(authorizationRequest.getSession());
return authorizationResponse;
}
private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response,
ClientRegistration registration) {
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
OAuth2AuthorizationRequest authorizationRequest = request()
.additionalParameters(additionalParameters)
.redirectUri(request.getRequestURL().toString()).build();
.attributes(attributes)
.redirectUri(UrlUtils.buildFullRequestUrl(request)).build();
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
}
@@ -57,6 +57,7 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.UnAuthenticatedServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
@@ -587,6 +588,43 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
verify(this.authorizedClientRepository).loadAuthorizedClient(eq(this.registration.getRegistrationId()), any(), eq(this.serverWebExchange));
}
// gh-7544
@Test
public void filterWhenClientCredentialsClientNotAuthorizedAndOutsideRequestContextThenGetNewToken() {
// Use UnAuthenticatedServerOAuth2AuthorizedClientRepository when operating outside of a request context
ServerOAuth2AuthorizedClientRepository unauthenticatedAuthorizedClientRepository = spy(new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
this.clientRegistrationRepository, unauthenticatedAuthorizedClientRepository);
this.function.setClientCredentialsTokenResponseClient(this.clientCredentialsTokenResponseClient);
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("new-token")
.tokenType(OAuth2AccessToken.TokenType.BEARER)
.expiresIn(360)
.build();
when(this.clientCredentialsTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
ClientRegistration registration = TestClientRegistrations.clientCredentials().build();
when(this.clientRegistrationRepository.findByRegistrationId(eq(registration.getRegistrationId()))).thenReturn(Mono.just(registration));
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com"))
.attributes(clientRegistrationId(registration.getRegistrationId()))
.build();
this.function.filter(request, this.exchange).block();
verify(unauthenticatedAuthorizedClientRepository).loadAuthorizedClient(any(), any(), any());
verify(this.clientCredentialsTokenResponseClient).getTokenResponse(any());
verify(unauthenticatedAuthorizedClientRepository).saveAuthorizedClient(any(), any(), any());
List<ClientRequest> requests = this.exchange.getRequests();
assertThat(requests).hasSize(1);
ClientRequest request1 = requests.get(0);
assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer new-token");
assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com");
assertThat(request1.method()).isEqualTo(HttpMethod.GET);
assertThat(getBody(request1)).isEmpty();
}
private Context serverWebExchange() {
return Context.of(ServerWebExchange.class, this.serverWebExchange);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -25,25 +25,32 @@ import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthorizationCodeAuthenticationTokens;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.handler.DefaultWebFilterChain;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests.request;
/**
* @author Rob Winch
@@ -102,7 +109,7 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
MockServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("/"));
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete());
e -> e.getResponse().setComplete(), Collections.emptyList());
this.filter.filter(exchange, chain).block();
@@ -111,43 +118,216 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
@Test
public void filterWhenMatchThenAuthorizedClientSaved() {
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
.redirectUri("/authorize/registration-id")
.build();
OAuth2AuthorizationResponse authorizationResponse = TestOAuth2AuthorizationResponses.success()
.redirectUri("/authorize/registration-id")
.build();
OAuth2AuthorizationExchange authorizationExchange =
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
ClientRegistration registration = TestClientRegistrations.clientRegistration().build();
Mono<Authentication> authentication = Mono.just(
new OAuth2AuthorizationCodeAuthenticationToken(registration, authorizationExchange));
OAuth2AuthorizationCodeAuthenticationToken authenticated = TestOAuth2AuthorizationCodeAuthenticationTokens
.authenticated();
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
when(this.clientRegistrationRepository.findByRegistrationId(any()))
.thenReturn(Mono.just(clientRegistration));
when(this.authenticationManager.authenticate(any())).thenReturn(
Mono.just(authenticated));
MockServerHttpRequest authorizationRequest =
createAuthorizationRequest("/authorization/callback");
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
.thenReturn(Mono.just(authorizationRequest));
.thenReturn(Mono.just(oauth2AuthorizationRequest));
when(this.authorizationRequestRepository.removeAuthorizationRequest(any()))
.thenReturn(Mono.just(oauth2AuthorizationRequest));
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
.thenReturn(Mono.empty());
ServerAuthenticationConverter converter = e -> authentication;
when(this.authenticationManager.authenticate(any()))
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
this.filter = new OAuth2AuthorizationCodeGrantWebFilter(
this.authenticationManager, converter, this.authorizedClientRepository);
this.filter.setAuthorizationRequestRepository(this.authorizationRequestRepository);
MockServerHttpRequest request = MockServerHttpRequest
.get("/authorize/registration-id")
.queryParam(OAuth2ParameterNames.CODE, "code")
.queryParam(OAuth2ParameterNames.STATE, "state")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete());
e -> e.getResponse().setComplete(), Collections.emptyList());
this.filter.filter(exchange, chain).block();
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), any(AnonymousAuthenticationToken.class), any());
}
// gh-7966
@Test
public void filterWhenAuthorizationRequestRedirectUriParametersMatchThenProcessed() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
when(this.clientRegistrationRepository.findByRegistrationId(any()))
.thenReturn(Mono.just(clientRegistration));
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
.thenReturn(Mono.empty());
when(this.authenticationManager.authenticate(any()))
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
// 1) redirect_uri with query parameters
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
MockServerHttpRequest authorizationRequest =
createAuthorizationRequest("/authorization/callback", parameters);
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
.thenReturn(Mono.just(oauth2AuthorizationRequest));
when(this.authorizationRequestRepository.removeAuthorizationRequest(any()))
.thenReturn(Mono.just(oauth2AuthorizationRequest));
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete(), Collections.emptyList());
this.filter.filter(exchange, chain).block();
verify(this.authenticationManager, times(1)).authenticate(any());
// 2) redirect_uri with query parameters AND authorization response additional parameters
Map<String, String> additionalParameters = new LinkedHashMap<>();
additionalParameters.put("auth-param1", "value1");
additionalParameters.put("auth-param2", "value2");
authorizationResponse = createAuthorizationResponse(authorizationRequest, additionalParameters);
exchange = MockServerWebExchange.from(authorizationResponse);
this.filter.filter(exchange, chain).block();
verify(this.authenticationManager, times(2)).authenticate(any());
}
// gh-7966
@Test
public void filterWhenAuthorizationRequestRedirectUriParametersNotMatchThenNotProcessed() {
String requestUri = "/authorization/callback";
Map<String, String> parameters = new LinkedHashMap<>();
parameters.put("param1", "value1");
parameters.put("param2", "value2");
MockServerHttpRequest authorizationRequest =
createAuthorizationRequest(requestUri, parameters);
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
.thenReturn(Mono.just(oauth2AuthorizationRequest));
// 1) Parameter value
Map<String, String> parametersNotMatch = new LinkedHashMap<>(parameters);
parametersNotMatch.put("param2", "value8");
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete(), Collections.emptyList());
this.filter.filter(exchange, chain).block();
verifyZeroInteractions(this.authenticationManager);
// 2) Parameter order
parametersNotMatch = new LinkedHashMap<>();
parametersNotMatch.put("param2", "value2");
parametersNotMatch.put("param1", "value1");
authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
exchange = MockServerWebExchange.from(authorizationResponse);
this.filter.filter(exchange, chain).block();
verifyZeroInteractions(this.authenticationManager);
// 3) Parameter missing
parametersNotMatch = new LinkedHashMap<>(parameters);
parametersNotMatch.remove("param2");
authorizationResponse = createAuthorizationResponse(
createAuthorizationRequest(requestUri, parametersNotMatch));
exchange = MockServerWebExchange.from(authorizationResponse);
this.filter.filter(exchange, chain).block();
verifyZeroInteractions(this.authenticationManager);
}
// gh-8609
@Test
public void filterWhenAuthenticationConverterThrowsOAuth2AuthorizationExceptionThenMappedToOAuth2AuthenticationException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(Mono.empty());
MockServerHttpRequest authorizationRequest =
createAuthorizationRequest("/authorization/callback");
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
.thenReturn(Mono.just(oauth2AuthorizationRequest));
when(this.authorizationRequestRepository.removeAuthorizationRequest(any()))
.thenReturn(Mono.just(oauth2AuthorizationRequest));
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete(), Collections.emptyList());
assertThatThrownBy(() -> this.filter.filter(exchange, chain).block())
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessageContaining("client_registration_not_found");
verifyZeroInteractions(this.authenticationManager);
}
// gh-8609
@Test
public void filterWhenAuthenticationManagerThrowsOAuth2AuthorizationExceptionThenMappedToOAuth2AuthenticationException() {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
when(this.clientRegistrationRepository.findByRegistrationId(any()))
.thenReturn(Mono.just(clientRegistration));
MockServerHttpRequest authorizationRequest =
createAuthorizationRequest("/authorization/callback");
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
when(this.authorizationRequestRepository.loadAuthorizationRequest(any()))
.thenReturn(Mono.just(oauth2AuthorizationRequest));
when(this.authorizationRequestRepository.removeAuthorizationRequest(any()))
.thenReturn(Mono.just(oauth2AuthorizationRequest));
when(this.authenticationManager.authenticate(any()))
.thenReturn(Mono.error(new OAuth2AuthorizationException(new OAuth2Error("authorization_error"))));
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
DefaultWebFilterChain chain = new DefaultWebFilterChain(
e -> e.getResponse().setComplete(), Collections.emptyList());
assertThatThrownBy(() -> this.filter.filter(exchange, chain).block())
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessageContaining("authorization_error");
}
private static OAuth2AuthorizationRequest createOAuth2AuthorizationRequest(
MockServerHttpRequest authorizationRequest, ClientRegistration registration) {
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
return request()
.attributes(attributes)
.redirectUri(authorizationRequest.getURI().toString())
.build();
}
private static MockServerHttpRequest createAuthorizationRequest(String requestUri) {
return createAuthorizationRequest(requestUri, new LinkedHashMap<>());
}
private static MockServerHttpRequest createAuthorizationRequest(String requestUri, Map<String, String> parameters) {
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest
.get(requestUri);
if (!CollectionUtils.isEmpty(parameters)) {
parameters.forEach(builder::queryParam);
}
return builder.build();
}
private static MockServerHttpRequest createAuthorizationResponse(MockServerHttpRequest authorizationRequest) {
return createAuthorizationResponse(authorizationRequest, new LinkedHashMap<>());
}
private static MockServerHttpRequest createAuthorizationResponse(
MockServerHttpRequest authorizationRequest, Map<String, String> additionalParameters) {
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest
.get(authorizationRequest.getURI().toString());
builder.queryParam(OAuth2ParameterNames.CODE, "code");
builder.queryParam(OAuth2ParameterNames.STATE, "state");
additionalParameters.forEach(builder::queryParam);
builder.cookies(authorizationRequest.getCookies());
return builder.build();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -23,6 +23,7 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
@@ -376,29 +377,34 @@ public final class OAuth2AuthorizationRequest implements Serializable {
private String buildAuthorizationRequestUri() {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, this.responseType.getValue());
parameters.set(OAuth2ParameterNames.CLIENT_ID, this.clientId);
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, encodeQueryParam(this.responseType.getValue()));
parameters.set(OAuth2ParameterNames.CLIENT_ID, encodeQueryParam(this.clientId));
if (!CollectionUtils.isEmpty(this.scopes)) {
parameters.set(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(this.scopes, " "));
encodeQueryParam(StringUtils.collectionToDelimitedString(this.scopes, " ")));
}
if (this.state != null) {
parameters.set(OAuth2ParameterNames.STATE, this.state);
parameters.set(OAuth2ParameterNames.STATE, encodeQueryParam(this.state));
}
if (this.redirectUri != null) {
parameters.set(OAuth2ParameterNames.REDIRECT_URI, this.redirectUri);
parameters.set(OAuth2ParameterNames.REDIRECT_URI, encodeQueryParam(this.redirectUri));
}
if (!CollectionUtils.isEmpty(this.additionalParameters)) {
this.additionalParameters.forEach((k, v) -> parameters.set(k, v.toString()));
this.additionalParameters.forEach((k, v) ->
parameters.set(encodeQueryParam(k), encodeQueryParam(v.toString())));
}
return UriComponentsBuilder.fromHttpUrl(this.authorizationUri)
.queryParams(parameters)
.encode(StandardCharsets.UTF_8)
.build()
.toUriString();
}
// Encode query parameter value according to RFC 3986
private static String encodeQueryParam(String value) {
return UriUtils.encodeQueryParam(value, StandardCharsets.UTF_8);
}
private LinkedHashSet<String> toLinkedHashSet(String... scope) {
LinkedHashSet<String> result = new LinkedHashSet<>();
Collections.addAll(result, scope);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -43,6 +43,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.stream.Collectors;
/**
* A {@link HttpMessageConverter} for an {@link OAuth2AccessTokenResponse OAuth 2.0 Access Token Response}.
@@ -55,8 +56,8 @@ import java.util.HashMap;
public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2AccessTokenResponse> {
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE =
new ParameterizedTypeReference<Map<String, String>>() {};
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
new ParameterizedTypeReference<Map<String, Object>>() {};
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
@@ -80,10 +81,16 @@ public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpM
throws HttpMessageNotReadableException {
try {
// gh-6463
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
@SuppressWarnings("unchecked")
Map<String, String> tokenResponseParameters = (Map<String, String>) this.jsonMessageConverter.read(
Map<String, Object> tokenResponseParameters = (Map<String, Object>) this.jsonMessageConverter.read(
PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage);
return this.tokenResponseConverter.convert(tokenResponseParameters);
return this.tokenResponseConverter.convert(
tokenResponseParameters.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().toString())));
} catch (Exception ex) {
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Access Token Response: " +
ex.getMessage(), ex, inputMessage);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -34,6 +34,7 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* A {@link HttpMessageConverter} for an {@link OAuth2Error OAuth 2.0 Error}.
@@ -46,8 +47,8 @@ import java.util.Map;
public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2Error> {
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE =
new ParameterizedTypeReference<Map<String, String>>() {};
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
new ParameterizedTypeReference<Map<String, Object>>() {};
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
@@ -69,10 +70,16 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte
throws HttpMessageNotReadableException {
try {
// gh-8157
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
@SuppressWarnings("unchecked")
Map<String, String> errorParameters = (Map<String, String>) this.jsonMessageConverter.read(
Map<String, Object> errorParameters = (Map<String, Object>) this.jsonMessageConverter.read(
PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage);
return this.errorConverter.convert(errorParameters);
return this.errorConverter.convert(
errorParameters.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> String.valueOf(entry.getValue()))));
} catch (Exception ex) {
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Error: " +
ex.getMessage(), ex, inputMessage);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -307,4 +307,38 @@ public class OAuth2AuthorizationRequestTests {
"response_type=code&client_id=client-id&state=state&" +
"redirect_uri=https://example.com/authorize/oauth2/code/registration-id");
}
@Test
public void buildWhenAuthorizationUriIncludesEscapedQueryParameterThenAuthorizationRequestUrlIncludesIt() {
OAuth2AuthorizationRequest authorizationRequest =
TestOAuth2AuthorizationRequests.request()
.authorizationUri(AUTHORIZATION_URI +
"?claims=%7B%22userinfo%22%3A%7B%22email_verified%22%3A%7B%22essential%22%3Atrue%7D%7D%7D").build();
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
assertThat(authorizationRequest.getAuthorizationRequestUri())
.isEqualTo("https://provider.com/oauth2/authorize?" +
"claims=%7B%22userinfo%22%3A%7B%22email_verified%22%3A%7B%22essential%22%3Atrue%7D%7D%7D&" +
"response_type=code&client_id=client-id&state=state&" +
"redirect_uri=https://example.com/authorize/oauth2/code/registration-id");
}
@Test
public void buildWhenNonAsciiAdditionalParametersThenProperlyEncoded() {
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put("item amount", "19.95" + '\u20ac');
additionalParameters.put("item name", "H" + '\u00c5' + "M" + '\u00d6');
additionalParameters.put('\u00e2' + "ge", "4" + '\u00bd');
OAuth2AuthorizationRequest authorizationRequest =
TestOAuth2AuthorizationRequests.request()
.additionalParameters(additionalParameters)
.build();
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
assertThat(authorizationRequest.getAuthorizationRequestUri())
.isEqualTo("https://example.com/login/oauth/authorize?" +
"response_type=code&client_id=client-id&state=state&" +
"redirect_uri=https://example.com/authorize/oauth2/code/registration-id&" +
"item%20amount=19.95%E2%82%AC&%C3%A2ge=4%C2%BD&item%20name=H%C3%85M%C3%96");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -96,6 +96,39 @@ public class OAuth2AccessTokenResponseHttpMessageConverterTests {
}
// gh-6463
@Test
public void readInternalWhenSuccessfulTokenResponseWithObjectThenReadOAuth2AccessTokenResponse() {
String tokenResponse = "{\n" +
" \"access_token\": \"access-token-1234\",\n" +
" \"token_type\": \"bearer\",\n" +
" \"expires_in\": 3600,\n" +
" \"scope\": \"read write\",\n" +
" \"refresh_token\": \"refresh-token-1234\",\n" +
" \"custom_object_1\": {\"name1\": \"value1\"},\n" +
" \"custom_object_2\": [\"value1\", \"value2\"],\n" +
" \"custom_parameter_1\": \"custom-value-1\",\n" +
" \"custom_parameter_2\": \"custom-value-2\"\n" +
"}\n";
MockClientHttpResponse response = new MockClientHttpResponse(
tokenResponse.getBytes(), HttpStatus.OK);
OAuth2AccessTokenResponse accessTokenResponse = this.messageConverter.readInternal(
OAuth2AccessTokenResponse.class, response);
assertThat(accessTokenResponse.getAccessToken().getTokenValue()).isEqualTo("access-token-1234");
assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBeforeOrEqualTo(Instant.now().plusSeconds(3600));
assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write");
assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo("refresh-token-1234");
assertThat(accessTokenResponse.getAdditionalParameters()).containsExactly(
entry("custom_object_1", "{name1=value1}"),
entry("custom_object_2", "[value1, value2]"),
entry("custom_parameter_1", "custom-value-1"),
entry("custom_parameter_2", "custom-value-2"));
}
@Test
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
Converter tokenResponseConverter = mock(Converter.class);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -78,6 +78,25 @@ public class OAuth2ErrorHttpMessageConverterTests {
assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2");
}
// gh-8157
@Test
public void readInternalWhenErrorResponseWithObjectThenReadOAuth2Error() throws Exception {
String errorResponse = "{\n" +
" \"error\": \"unauthorized_client\",\n" +
" \"error_description\": \"The client is not authorized\",\n" +
" \"error_codes\": [65001],\n" +
" \"error_uri\": \"https://tools.ietf.org/html/rfc6749#section-5.2\"\n" +
"}\n";
MockClientHttpResponse response = new MockClientHttpResponse(
errorResponse.getBytes(), HttpStatus.BAD_REQUEST);
OAuth2Error oauth2Error = this.messageConverter.readInternal(OAuth2Error.class, response);
assertThat(oauth2Error.getErrorCode()).isEqualTo("unauthorized_client");
assertThat(oauth2Error.getDescription()).isEqualTo("The client is not authorized");
assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2");
}
@Test
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
Converter errorConverter = mock(Converter.class);
@@ -15,6 +15,9 @@
*/
package org.springframework.security.oauth2.jwt;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
@@ -28,6 +31,8 @@ import org.springframework.util.Assert;
* @since 5.1
*/
public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
private final Log logger = LogFactory.getLog(getClass());
private static OAuth2Error INVALID_ISSUER =
new OAuth2Error(
OAuth2ErrorCodes.INVALID_REQUEST,
@@ -57,6 +62,7 @@ public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
if (this.issuer.equals(tokenIssuer)) {
return OAuth2TokenValidatorResult.success();
} else {
logger.debug(INVALID_ISSUER.getDescription());
return OAuth2TokenValidatorResult.failure(INVALID_ISSUER);
}
}
@@ -20,6 +20,9 @@ import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
@@ -41,6 +44,8 @@ import org.springframework.util.Assert;
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
*/
public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
private final Log logger = LogFactory.getLog(getClass());
private static final Duration DEFAULT_MAX_CLOCK_SKEW = Duration.of(60, ChronoUnit.SECONDS);
private final Duration clockSkew;
@@ -56,7 +61,6 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
public JwtTimestampValidator(Duration clockSkew) {
Assert.notNull(clockSkew, "clockSkew cannot be null");
this.clockSkew = clockSkew;
}
@@ -71,11 +75,8 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
if (expiry != null) {
if (Instant.now(this.clock).minus(clockSkew).isAfter(expiry)) {
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_REQUEST,
String.format("Jwt expired at %s", jwt.getExpiresAt()),
"https://tools.ietf.org/html/rfc6750#section-3.1");
return OAuth2TokenValidatorResult.failure(error);
OAuth2Error oAuth2Error = createOAuth2Error(String.format("Jwt expired at %s", jwt.getExpiresAt()));
return OAuth2TokenValidatorResult.failure(oAuth2Error);
}
}
@@ -83,17 +84,22 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
if (notBefore != null) {
if (Instant.now(this.clock).plus(clockSkew).isBefore(notBefore)) {
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_REQUEST,
String.format("Jwt used before %s", jwt.getNotBefore()),
"https://tools.ietf.org/html/rfc6750#section-3.1");
return OAuth2TokenValidatorResult.failure(error);
OAuth2Error oAuth2Error = createOAuth2Error(String.format("Jwt used before %s", jwt.getNotBefore()));
return OAuth2TokenValidatorResult.failure(oAuth2Error);
}
}
return OAuth2TokenValidatorResult.success();
}
private OAuth2Error createOAuth2Error(String reason) {
logger.debug(reason);
return new OAuth2Error(
OAuth2ErrorCodes.INVALID_REQUEST,
reason,
"https://tools.ietf.org/html/rfc6750#section-3.1");
}
/**
* '
* Use this {@link Clock} with {@link Instant#now()} for assessing
@@ -22,6 +22,7 @@ import java.net.URL;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -54,11 +55,13 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
@@ -164,9 +167,17 @@ public final class NimbusJwtDecoder implements JwtDecoder {
private Jwt validateJwt(Jwt jwt){
OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt);
if (result.hasErrors()) {
String description = result.getErrors().iterator().next().getDescription();
Collection<OAuth2Error> errors = result.getErrors();
String validationErrorString = "Unable to validate Jwt";
for (OAuth2Error oAuth2Error : errors) {
if (!StringUtils.isEmpty(oAuth2Error.getDescription())) {
validationErrorString = String.format(
DECODING_ERROR_MESSAGE_TEMPLATE, oAuth2Error.getDescription());
break;
}
}
throw new JwtValidationException(
String.format(DECODING_ERROR_MESSAGE_TEMPLATE, description),
validationErrorString,
result.getErrors());
}
@@ -16,6 +16,7 @@
package org.springframework.security.oauth2.jwt;
import java.security.interfaces.RSAPublicKey;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -51,12 +52,14 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
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.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
/**
@@ -172,10 +175,16 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
private Jwt validateJwt(Jwt jwt) {
OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt);
if ( result.hasErrors() ) {
String message = result.getErrors().iterator().next().getDescription();
throw new JwtValidationException(message, result.getErrors());
if (result.hasErrors()) {
Collection<OAuth2Error> errors = result.getErrors();
String validationErrorString = "Unable to validate Jwt";
for (OAuth2Error oAuth2Error : errors) {
if (!StringUtils.isEmpty(oAuth2Error.getDescription())) {
validationErrorString = oAuth2Error.getDescription();
break;
}
}
throw new JwtValidationException(validationErrorString, errors);
}
return jwt;
@@ -193,6 +193,22 @@ public class NimbusJwtDecoderTests {
.hasFieldOrPropertyWithValue("errors", Arrays.asList(firstFailure, secondFailure));
}
@Test
public void decodeWhenReadingErrorPickTheFirstErrorMessage() {
OAuth2TokenValidator<Jwt> jwtValidator = mock(OAuth2TokenValidator.class);
this.jwtDecoder.setJwtValidator(jwtValidator);
OAuth2Error errorEmpty = new OAuth2Error("mock-error", "", "mock-uri");
OAuth2Error error = new OAuth2Error("mock-error", "mock-description", "mock-uri");
OAuth2Error error2 = new OAuth2Error("mock-error-second", "mock-description-second", "mock-uri-second");
OAuth2TokenValidatorResult result = OAuth2TokenValidatorResult.failure(errorEmpty, error, error2);
when(jwtValidator.validate(any(Jwt.class))).thenReturn(result);
Assertions.assertThatCode(() -> this.jwtDecoder.decode(SIGNED_JWT))
.isInstanceOf(JwtValidationException.class)
.hasMessageContaining("mock-description");
}
@Test
public void decodeWhenUsingSignedJwtThenReturnsClaimsGivenByClaimSetConverter() {
Converter<Map<String, Object>, Map<String, Object>> claimSetConverter = mock(Converter.class);
@@ -221,6 +221,22 @@ public class NimbusReactiveJwtDecoderTests {
.hasMessageContaining("mock-description");
}
@Test
public void decodeWhenReadingErrorPickTheFirstErrorMessage() {
OAuth2TokenValidator<Jwt> jwtValidator = mock(OAuth2TokenValidator.class);
this.decoder.setJwtValidator(jwtValidator);
OAuth2Error errorEmpty = new OAuth2Error("mock-error", "", "mock-uri");
OAuth2Error error = new OAuth2Error("mock-error", "mock-description", "mock-uri");
OAuth2Error error2 = new OAuth2Error("mock-error-second", "mock-description-second", "mock-uri-second");
OAuth2TokenValidatorResult result = OAuth2TokenValidatorResult.failure(errorEmpty, error, error2);
when(jwtValidator.validate(any(Jwt.class))).thenReturn(result);
assertThatCode(() -> this.decoder.decode(this.messageReadToken).block())
.isInstanceOf(JwtValidationException.class)
.hasMessageContaining("mock-description");
}
@Test
public void decodeWhenUsingSignedJwtThenReturnsClaimsGivenByClaimSetConverter() {
Converter<Map<String, Object>, Map<String, Object>> claimSetConverter = mock(Converter.class);

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