From 9db33f33c711dee8ba7ca9650f23968c1824e128 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Tue, 31 Oct 2023 15:11:45 -0500 Subject: [PATCH] Revert unnecessary merges on 6.0.x This commit removes unnecessary main-branch merges starting from 8750608b5bca45525c99d0a41a20ed02de93d8c7 and adds the following needed commit(s) that were made afterward: - 5dce82c48bc0b174838501c5a111b2de70822914 --- .github/dependabot.yml | 92 - .../continuous-integration-workflow.yml | 11 +- .github/workflows/deploy-docs.yml | 4 +- .../gradle-wrapper-upgrade-execution.yml | 32 - .github/workflows/release-scheduler.yml | 2 +- CONTRIBUTING.adoc | 314 +- RELEASE.adoc | 2 +- .../security/acls/AclPermissionEvaluator.java | 3 +- .../acls/domain/AbstractPermission.java | 3 +- .../acls/jdbc/JdbcAclServiceTests.java | 12 +- .../acls/jdbc/JdbcMutableAclServiceTests.java | 2 +- .../security/acls/sid/SidTests.java | 4 +- aspects/spring-security-aspects.gradle | 6 + .../aspect/AnnotationSecurityAspectTests.java | 4 +- build.gradle | 42 +- buildSrc/build.gradle | 2 - .../gradle/antora/AntoraVersionPlugin.java | 9 +- ...sspathForProhibitedDependenciesPlugin.java | 4 +- ...ProhibitedDependenciesLifecyclePlugin.java | 4 +- .../changelog/GitHubChangelogPlugin.java | 8 +- .../milestones/SpringReleaseTrainSpec.java | 40 +- .../gradle/maven/SpringSigningPlugin.java | 8 +- cas/spring-security-cas.gradle | 25 - .../security/cas/SamlServiceProperties.java | 37 - .../security/cas/ServiceProperties.java | 132 - .../CasAssertionAuthenticationToken.java | 60 - .../CasAuthenticationProvider.java | 235 - .../CasAuthenticationToken.java | 173 - .../CasServiceTicketAuthenticationToken.java | 112 - .../NullStatelessTicketCache.java | 64 - .../SpringCacheBasedTicketCache.java | 69 - .../authentication/StatelessTicketCache.java | 110 - .../cas/authentication/package-info.java | 21 - .../cas/jackson2/AssertionImplMixin.java | 69 - .../jackson2/AttributePrincipalImplMixin.java | 66 - .../jackson2/CasAuthenticationTokenMixin.java | 83 - .../cas/jackson2/CasJackson2Module.java | 58 - .../security/cas/package-info.java | 21 - ...bstractCasAssertionUserDetailsService.java | 51 - ...AssertionAttributesUserDetailsService.java | 87 - .../cas/web/CasAuthenticationEntryPoint.java | 152 - .../cas/web/CasAuthenticationFilter.java | 395 -- .../DefaultServiceAuthenticationDetails.java | 146 - .../ServiceAuthenticationDetails.java | 42 - .../ServiceAuthenticationDetailsSource.java | 83 - .../cas/web/authentication/package-info.java | 21 - .../security/cas/web/package-info.java | 20 - .../AbstractStatelessTicketCacheTests.java | 45 - .../CasAuthenticationProviderTests.java | 376 -- .../CasAuthenticationTokenTests.java | 158 - .../NullStatelessTicketCacheTests.java | 46 - .../SpringCacheBasedTicketCacheTests.java | 65 - .../CasAuthenticationTokenMixinTests.java | 154 - ...tionAttributesUserDetailsServiceTests.java | 63 - .../web/CasAuthenticationEntryPointTests.java | 98 - .../cas/web/CasAuthenticationFilterTests.java | 222 - .../cas/web/ServicePropertiesTests.java | 67 - ...aultServiceAuthenticationDetailsTests.java | 132 - cas/src/test/resources/logback-test.xml | 15 - ...tserviceauthenticationdetails-explicit.xml | 23 - ...serviceauthenticationdetails-passivity.xml | 15 - config/spring-security-config.gradle | 2 +- .../rsocket/RSocketMessageHandlerITests.java | 2 +- ...pUserServiceBeanDefinitionParserTests.java | 4 +- .../config/SecurityNamespaceHandler.java | 4 +- .../AbstractConfiguredSecurityBuilder.java | 23 +- .../annotation/SecurityConfigurerAdapter.java | 4 +- .../configuration/MethodSecuritySelector.java | 3 +- .../web/AbstractRequestMatcherRegistry.java | 145 +- .../web/builders/FilterOrderRegistration.java | 8 +- .../annotation/web/builders/HttpSecurity.java | 271 +- .../web/configuration/EnableWebSecurity.java | 2 +- .../HttpSecurityConfiguration.java | 11 +- .../OAuth2ClientConfiguration.java | 347 +- ...bstractAuthenticationFilterConfigurer.java | 9 +- ...AbstractRequestMatcherBuilderRegistry.java | 52 - .../AntPathRequestMatcherBuilder.java | 59 - .../AuthorizeHttpRequestsConfigurer.java | 441 +- .../ChannelSecurityConfigurer.java | 6 +- .../web/configurers/CsrfConfigurer.java | 7 +- ...ervletDelegatingRequestMatcherBuilder.java | 103 - .../web/configurers/HeadersConfigurer.java | 115 +- .../web/configurers/HttpBasicConfigurer.java | 23 +- .../configurers/MvcRequestMatcherBuilder.java | 76 - .../configurers/RequestMatcherBuilder.java | 106 - .../configurers/RequestMatcherBuilders.java | 215 - .../ServletPatternRequestMatcher.java | 43 - .../ServletRegistrationCollection.java | 152 - .../SessionManagementConfigurer.java | 7 +- ...efaultOidcLogoutTokenValidatorFactory.java | 35 - .../oauth2/client/OAuth2ClientConfigurer.java | 27 +- .../client/OAuth2ClientConfigurerUtils.java | 11 - .../oauth2/client/OAuth2LoginConfigurer.java | 191 +- .../OidcBackChannelLogoutAuthentication.java | 66 - ...ckChannelLogoutAuthenticationProvider.java | 114 - .../client/OidcBackChannelLogoutFilter.java | 139 - .../client/OidcBackChannelLogoutHandler.java | 177 - .../OidcBackChannelLogoutTokenValidator.java | 118 - .../OidcLogoutAuthenticationConverter.java | 85 - .../client/OidcLogoutAuthenticationToken.java | 80 - .../oauth2/client/OidcLogoutConfigurer.java | 159 - .../OAuth2ResourceServerConfigurer.java | 24 +- .../saml2/Saml2LoginConfigurer.java | 36 +- .../saml2/Saml2LogoutConfigurer.java | 73 +- .../saml2/Saml2MetadataConfigurer.java | 169 - .../RsaKeyConversionServicePostProcessor.java | 3 +- .../http/AuthenticationConfigBuilder.java | 4 +- .../config/http/ChannelAttributeFactory.java | 19 +- .../config/http/CsrfBeanDefinitionParser.java | 16 +- ...Auth2AuthorizedClientManagerRegistrar.java | 287 -- .../OAuth2ClientBeanDefinitionParser.java | 6 +- ...OAuth2ClientBeanDefinitionParserUtils.java | 6 +- .../http/OAuth2LoginBeanDefinitionParser.java | 24 +- ...th2ResourceServerBeanDefinitionParser.java | 12 +- ...ientRegistrationsBeanDefinitionParser.java | 2 +- ...efaultOidcLogoutTokenValidatorFactory.java | 35 - .../OidcBackChannelLogoutAuthentication.java | 66 - ...elLogoutReactiveAuthenticationManager.java | 111 - .../OidcBackChannelLogoutTokenValidator.java | 118 - .../OidcBackChannelLogoutWebFilter.java | 137 - .../OidcBackChannelServerLogoutHandler.java | 181 - .../server/OidcLogoutAuthenticationToken.java | 80 - ...dcLogoutServerAuthenticationConverter.java | 90 - .../config/web/server/ServerHttpSecurity.java | 651 +-- .../config/annotation/web/FormLoginDsl.kt | 16 +- .../config/annotation/web/HttpSecurityDsl.kt | 107 +- .../config/annotation/web/OidcLogoutDsl.kt | 75 - .../config/annotation/web/Saml2MetadataDsl.kt | 52 - .../oauth2/login/OidcBackChannelLogoutDsl.kt | 34 - .../web/server/ServerHttpSecurityDsl.kt | 34 +- .../server/ServerOidcBackChannelLogoutDsl.kt | 30 - .../config/web/server/ServerOidcLogoutDsl.kt | 71 - .../main/resources/META-INF/spring.schemas | 8 +- .../security/config/spring-security-6.1.rnc | 1346 ------ .../security/config/spring-security-6.1.xsd | 3812 ----------------- .../security/config/spring-security-6.2.rnc | 1346 ------ .../security/config/spring-security-6.2.xsd | 3812 ----------------- .../security/config/MockServletContext.java | 5 - .../config/TestMockHttpServletMappings.java | 46 - .../UserDetailsManagerConfigurerTests.java | 6 +- .../annotation/issue50/SecurityConfig.java | 2 +- .../EnableCustomMethodSecurity.java | 41 - ...ePostMethodSecurityConfigurationTests.java | 26 - ...bstractConfiguredSecurityBuilderTests.java | 16 +- ...tractRequestMatcherRegistryNoMvcTests.java | 13 +- .../AbstractRequestMatcherRegistryTests.java | 116 +- .../web/builders/HttpConfigurationTests.java | 32 - .../HttpSecurityConfigurationTests.java | 100 +- ...orizedClientManagerConfigurationTests.java | 547 --- .../OAuth2ClientConfigurationTests.java | 5 +- ...urityReactorContextConfigurationTests.java | 63 +- ...actRequestMatcherBuilderRegistryTests.java | 349 -- .../AuthorizeHttpRequestsConfigurerTests.java | 477 +-- .../web/configurers/DefaultFiltersTests.java | 28 +- .../DefaultLoginPageConfigurerTests.java | 10 +- .../HeadersConfigurerEagerHeadersTests.java | 14 +- .../configurers/HttpBasicConfigurerTests.java | 38 +- .../RequestCacheConfigurerTests.java | 2 +- .../RequestMatcherBuildersTests.java | 198 - .../ServletPatternRequestMatcherTests.java | 64 - .../SessionManagementConfigurerTests.java | 4 +- .../client/OidcLogoutConfigurerTests.java | 596 --- .../OAuth2ResourceServerConfigurerTests.java | 7 +- .../saml2/Saml2LoginConfigurerTests.java | 59 +- .../saml2/Saml2MetadataConfigurerTests.java | 215 - ...WebSocketMessageBrokerConfigurerTests.java | 4 +- ...cutorSubscribableChannelPostProcessor.java | 3 +- ...ssageBrokerSecurityConfigurationTests.java | 7 +- .../config/doc/SpringSecurityXsdParser.java | 4 +- .../security/config/doc/XmlNode.java | 5 +- .../config/doc/XsdDocumentedTests.java | 9 +- .../FormLoginBeanDefinitionParserTests.java | 6 +- .../config/http/MiscHttpConfigTests.java | 2 +- ...AuthorizedClientManagerRegistrarTests.java | 475 -- ...OAuth2ClientBeanDefinitionParserTests.java | 11 +- .../OAuth2LoginBeanDefinitionParserTests.java | 11 +- .../Saml2LoginBeanDefinitionParserTests.java | 54 +- .../test/SpringTestContextExtension.java | 2 +- .../web/server/OAuth2ClientSpecTests.java | 9 +- .../web/server/OidcLogoutSpecTests.java | 685 --- .../web/server/ServerHttpSecurityTests.java | 2 +- ...thodSecurityNoAuthorizationManagerTests.kt | 37 +- .../web/AuthorizeHttpRequestsDslTests.kt | 18 +- .../web/AuthorizeRequestsDslTests.kt | 24 +- .../config/annotation/web/CorsDslTests.kt | 15 +- .../config/annotation/web/CsrfDslTests.kt | 6 +- .../annotation/web/FormLoginDslTests.kt | 45 +- .../annotation/web/HttpBasicDslTests.kt | 3 +- .../annotation/web/HttpSecurityDslTests.kt | 73 +- .../annotation/web/OAuth2LoginDslTests.kt | 4 +- .../annotation/web/OidcLogoutDslTests.kt | 87 - .../annotation/web/Saml2MetadataDslTests.kt | 189 - .../web/server/ServerOidcLogoutDslTests.kt | 97 - ...ConfigTests-mock-csrf-token-repository.xml | 4 +- .../CsrfConfigTests-mock-request-matcher.xml | 4 +- .../HttpConfigTests-AuthorizationManager.xml | 4 +- ...ttpConfigTests-WithObservationRegistry.xml | 2 +- .../MiscHttpConfigTests-CollidingFilters.xml | 2 +- ...s-CustomAuthenticationDetailsSourceRef.xml | 2 +- .../MiscHttpConfigTests-CustomFilters.xml | 2 +- ...nfigTests-CustomHttpBasicEntryPointRef.xml | 2 +- .../http/MiscHttpConfigTests-EntryPoint.xml | 2 +- ...ests-ExplicitSaveAndExplicitRepository.xml | 2 +- .../MiscHttpConfigTests-ExpressionHandler.xml | 2 +- .../http/MiscHttpConfigTests-HttpFirewall.xml | 2 +- .../config/http/MiscHttpConfigTests-Jaas.xml | 2 +- .../http/MiscHttpConfigTests-RequestCache.xml | 2 +- ...HttpConfigTests-RequestRejectedHandler.xml | 4 +- .../http/MiscHttpConfigTests-Sec750.xml | 2 +- ...pConfigTests-SecurityContextRepository.xml | 2 +- ...zedClientManagerRegistrarTests-clients.xml | 56 - ...zedClientManagerRegistrarTests-minimal.xml | 41 - ...dClientManagerRegistrarTests-providers.xml | 59 - ...Tests-AuthorizedClientArgumentResolver.xml | 4 +- ...ts-CustomAuthorizationRedirectStrategy.xml | 4 +- ...serTests-CustomAuthorizedClientService.xml | 16 +- ...sts-CustomClientRegistrationRepository.xml | 4 +- ...initionParserTests-CustomConfiguration.xml | 16 +- ...Tests-AuthorizedClientArgumentResolver.xml | 4 +- ...ntRegistration-WithCustomConfiguration.xml | 20 +- ...istration-WithCustomGrantedAuthorities.xml | 24 +- ...istration-WithCustomLoginProcessingUrl.xml | 16 +- ...WithCustomAuthenticationFailureHandler.xml | 4 +- ...ithCustomAuthorizationRedirectStrategy.xml | 4 +- ...WithCustomAuthorizationRequestResolver.xml | 4 +- ...ientRegistration-WithJwtDecoderFactory.xml | 20 +- ...DecoderFactoryAndDefaultSuccessHandler.xml | 16 +- ...onParserTests-SingleClientRegistration.xml | 4 +- ...s-WithCustomAuthorizedClientRepository.xml | 20 +- ...ests-WithCustomAuthorizedClientService.xml | 20 +- ...WithCustomClientRegistrationRepository.xml | 16 +- ...ithCustomSecurityContextHolderStrategy.xml | 20 +- ...serTests-AuthenticationManagerResolver.xml | 2 +- ...ticationManagerResolverPlusOtherConfig.xml | 4 +- ...initionParserTests-ExpiredJwtClockSkew.xml | 2 +- ...tionParserTests-JwtDecoderAndJwkSetUri.xml | 2 +- ...efinitionParserTests-JwtRestOperations.xml | 2 +- ...ionParserTests-MockBearerTokenResolver.xml | 2 +- ...erTests-MockJwtAuthenticationConverter.xml | 2 +- ...anDefinitionParserTests-MockJwtDecoder.xml | 2 +- ...DefinitionParserTests-MockJwtValidator.xml | 4 +- ...arserTests-MockOpaqueTokenIntrospector.xml | 2 +- ...-OpaqueTokenAndAuthenticationConverter.xml | 5 +- ...erTests-OpaqueTokenAndIntrospectionUri.xml | 2 +- ...nParserTests-OpaqueTokenRestOperations.xml | 2 +- ...itionParserTests-UnexpiredJwtClockSkew.xml | 2 +- ...erMeConfigTests-WithUserDetailsService.xml | 4 +- ...WithCustomAuthenticationFailureHandler.xml | 6 +- ...erTests-SingleRelyingPartyRegistration.xml | 6 +- ...gUrl-WithCustomAuthenticationConverter.xml | 8 +- ...tory-WithCustomAuthenticationConverter.xml | 12 +- ...sitory-WithCustomAuthenticationManager.xml | 12 +- ...ithCustomAuthenticationRequestResolver.xml | 12 +- ...itory-WithCustomAuthnRequestRepository.xml | 12 +- ...Tests-WithCustomRelyingPartyRepository.xml | 16 +- ...ithCustomSecurityContextHolderStrategy.xml | 12 +- ...-CsrfDisabled-MockLogoutSuccessHandler.xml | 8 +- ...DefinitionParserTests-CustomComponents.xml | 32 +- .../security/config/method-security.xml | 6 +- ...Tests-CustomAuthorizationManagerConfig.xml | 2 +- core/spring-security-core.gradle | 1 + .../security/access/SecurityConfig.java | 3 +- .../Jsr250MethodSecurityMetadataSource.java | 3 +- .../expression/SecurityExpressionRoot.java | 34 +- .../AbstractMethodSecurityMetadataSource.java | 3 +- .../MapBasedMethodSecurityMetadataSource.java | 3 +- ...rePostAdviceReactiveMethodInterceptor.java | 34 +- .../security/access/vote/ConsensusBased.java | 12 +- .../AbstractAuthenticationToken.java | 15 +- .../AnonymousAuthenticationToken.java | 3 +- .../AuthenticationTrustResolver.java | 17 - .../ObservationAuthenticationManager.java | 16 +- ...ervationReactiveAuthenticationManager.java | 17 +- .../authentication/ProviderManager.java | 3 +- .../RememberMeAuthenticationToken.java | 8 +- .../TestingAuthenticationToken.java | 9 +- .../AbstractJaasAuthenticationProvider.java | 6 +- .../jaas/JaasGrantedAuthority.java | 5 +- .../jaas/SecurityContextLoginModule.java | 3 + .../AuthenticatedAuthorizationManager.java | 2 +- .../AuthoritiesAuthorizationManager.java | 81 - .../AuthorityAuthorizationManager.java | 34 +- .../authorization/AuthorizationManager.java | 2 +- .../AuthorizationObservationConvention.java | 3 - .../ObservationAuthorizationManager.java | 38 +- ...servationReactiveAuthorizationManager.java | 17 +- .../method/Jsr250AuthorizationManager.java | 35 +- .../method/SecuredAuthorizationManager.java | 63 +- .../security/core/ComparableVersion.java | 141 +- .../core/SpringSecurityCoreVersion.java | 2 +- .../core/authority/AuthorityUtils.java | 17 +- .../authority/SimpleGrantedAuthority.java | 6 +- ...edAttributes2GrantedAuthoritiesMapper.java | 1 + .../SecurityContextHolderStrategy.java | 2 +- .../core/context/SecurityContextImpl.java | 3 +- .../AnnotationParameterNameDiscoverer.java | 1 + ...efaultSecurityParameterNameDiscoverer.java | 6 + .../core/session/SessionRegistryImpl.java | 6 +- .../security/core/token/DefaultToken.java | 5 +- .../KeyBasedPersistenceTokenService.java | 2 +- .../security/core/userdetails/User.java | 13 +- .../jackson2/SecurityJackson2Modules.java | 2 +- .../UnmodifiableListDeserializer.java | 3 +- .../jackson2/UnmodifiableSetDeserializer.java | 3 +- .../security/util/MethodInvocationUtils.java | 3 +- .../DelegatingSecurityContextTestUtils.java | 102 - .../RunAsImplAuthenticationProviderTests.java | 3 +- .../intercept/RunAsManagerImplTests.java | 12 +- ...tingMethodSecurityMetadataSourceTests.java | 4 +- .../security/access/vote/DenyAgainVoter.java | 5 +- .../security/access/vote/DenyVoter.java | 5 +- ...ObservationAuthenticationManagerTests.java | 8 +- ...ionReactiveAuthenticationManagerTests.java | 8 +- .../authentication/TestAuthentication.java | 18 +- ...efaultJaasAuthenticationProviderTests.java | 7 +- .../jaas/JaasAuthenticationProviderTests.java | 3 +- .../jaas/TestCallbackHandler.java | 3 +- .../memory/InMemoryConfigurationTests.java | 2 +- .../AuthoritiesAuthorizationManagerTests.java | 87 - .../AuthorityAuthorizationManagerTests.java | 4 +- .../ObservationAuthorizationManagerTests.java | 17 +- ...tionReactiveAuthorizationManagerTests.java | 8 +- ...pringAuthorizationEventPublisherTests.java | 2 +- .../Jsr250AuthorizationManagerTests.java | 56 +- .../SecuredAuthorizationManagerTests.java | 28 +- ...curityContextExecutorIntegrationTests.java | 75 - ...ontextExecutorServiceIntegrationTests.java | 98 - ...eduledExecutorServiceIntegrationTests.java | 121 - .../security/core/JavaVersionTests.java | 10 +- .../core/StaticFinalReflectionUtils.java | 8 +- .../core/authority/AuthorityUtilsTests.java | 24 +- .../mapping/SimpleAuthoritiesMapperTests.java | 18 +- .../ReactiveSecurityContextHolderTests.java | 58 +- ...calSecurityContextHolderStrategyTests.java | 2 +- ...tSecurityParameterNameDiscovererTests.java | 6 +- .../session/SessionRegistryImplTests.java | 4 +- .../security/core/userdetails/UserTests.java | 70 - .../JdbcUserDetailsManagerTests.java | 8 +- ...chedulingTaskExecutorIntegrationTests.java | 148 - ...yContextTaskSchedulerIntegrationTests.java | 125 - ...textAsyncTaskExecutorIntegrationTests.java | 143 - ...tyContextTaskExecutorIntegrationTests.java | 75 - .../crypto/argon2/Argon2EncodingUtils.java | 39 +- .../security/crypto/bcrypt/BCryptTests.java | 2 +- ...stleAesBytesEncryptorEquivalencyTests.java | 13 +- .../BouncyCastleAesBytesEncryptorTests.java | 8 +- .../crypto/keygen/KeyGeneratorsTests.java | 2 +- .../password/Pbkdf2PasswordEncoderTests.java | 4 +- .../spring-security-dependencies.gradle | 2 - .../multi-securityfilterchain.odg | Bin 22677 -> 14601 bytes .../multi-securityfilterchain.png | Bin 72263 -> 57970 bytes .../servlet/authorization/methodsecurity.odg | Bin 38248 -> 0 bytes .../servlet/authorization/methodsecurity.png | Bin 186701 -> 0 bytes docs/modules/ROOT/nav.adoc | 16 +- .../ROOT/pages/features/exploits/http.adoc | 10 +- .../pages/features/integrations/data.adoc | 2 +- .../ROOT/pages/migration-7/configuration.adoc | 125 - .../modules/ROOT/pages/migration-7/index.adoc | 8 - docs/modules/ROOT/pages/migration-7/ldap.adoc | 11 - .../ROOT/pages/migration/authorization.adoc | 24 - docs/modules/ROOT/pages/migration/index.adoc | 34 +- .../ROOT/pages/migration/reactive.adoc | 100 + .../migration/servlet/authentication.adoc | 187 + .../migration/servlet/authorization.adoc | 117 + .../pages/migration/servlet/exploits.adoc | 44 + .../ROOT/pages/migration/servlet/index.adoc | 4 + .../migration/servlet/session-management.adoc | 49 + .../reactive/integrations/observability.adoc | 4 +- .../pages/reactive/oauth2/client/index.adoc | 2 - .../pages/reactive/oauth2/login/advanced.adoc | 108 +- .../pages/reactive/oauth2/login/logout.adoc | 267 -- .../reactive/oauth2/resource-server/jwt.adoc | 85 +- .../oauth2/resource-server/multitenancy.adoc | 7 +- .../oauth2/resource-server/opaque-token.adoc | 20 +- .../servlet/appendix/namespace/http.adoc | 2 +- .../servlet/appendix/namespace/index.adoc | 2 +- .../ROOT/pages/servlet/architecture.adoc | 21 +- .../pages/servlet/authentication/cas.adoc | 466 -- .../pages/servlet/authentication/index.adoc | 1 - .../pages/servlet/authentication/logout.adoc | 502 +-- .../authentication/passwords/basic.adoc | 2 +- .../authentication/passwords/form.adoc | 2 +- .../authentication/session-management.adoc | 26 +- .../pages/servlet/authorization/acls.adoc | 2 +- .../servlet/authorization/architecture.adoc | 96 +- .../authorize-http-requests.adoc | 1313 ++---- .../authorization/authorize-requests.adoc | 194 + .../authorization/expression-based.adoc | 550 +++ .../pages/servlet/authorization/index.adoc | 7 +- .../authorization/method-security.adoc | 2348 ++++------ .../servlet/authorization/secure-objects.adoc | 135 + .../pages/servlet/configuration/java.adoc | 105 +- .../servlet/configuration/xml-namespace.adoc | 4 +- .../ROOT/pages/servlet/exploits/firewall.adoc | 2 +- .../ROOT/pages/servlet/getting-started.adoc | 185 +- .../ROOT/pages/servlet/integrations/cors.adoc | 136 +- .../ROOT/pages/servlet/integrations/data.adoc | 2 +- .../servlet/integrations/jsp-taglibs.adoc | 2 +- .../servlet/integrations/observability.adoc | 2 +- .../pages/servlet/integrations/websocket.adoc | 42 +- .../ROOT/pages/servlet/oauth2/index.adoc | 1686 +------- .../pages/servlet/oauth2/login/advanced.adoc | 108 +- .../pages/servlet/oauth2/login/logout.adoc | 267 -- .../servlet/oauth2/resource-server/index.adoc | 2 +- .../servlet/oauth2/resource-server/jwt.adoc | 103 +- .../oauth2/resource-server/multitenancy.adoc | 6 +- .../oauth2/resource-server/opaque-token.adoc | 20 +- .../servlet/saml2/login/authentication.adoc | 84 +- .../pages/servlet/saml2/login/overview.adoc | 147 +- .../ROOT/pages/servlet/saml2/logout.adoc | 10 +- .../ROOT/pages/servlet/saml2/metadata.adoc | 102 +- .../test/mockmvc/request-builders.adoc | 2 +- .../servlet/test/mockmvc/result-handlers.adoc | 4 +- .../servlet/test/mockmvc/result-matchers.adoc | 6 +- docs/modules/ROOT/pages/whats-new.adoc | 68 +- docs/spring-security-docs.gradle | 2 - etc/nohttp/allowlist.lines | 3 +- git/hooks/.forward-merge.swp | Bin 16384 -> 0 bytes git/hooks/prepare-forward-merge | 2 +- gradle.properties | 13 +- gradle/libs.versions.toml | 54 +- .../PythonInterpreterPreInvocationAdvice.java | 4 +- .../SpringSecurityLdapTemplateITests.java | 8 +- ...esPopulatorGetGrantedAuthoritiesTests.java | 4 +- .../DefaultLdapAuthoritiesPopulatorTests.java | 24 +- .../NestedLdapAuthoritiesPopulatorTests.java | 4 +- ...veDirectoryLdapAuthenticationProvider.java | 54 +- .../ldap/server/ApacheDSContainer.java | 6 +- .../ldap/userdetails/LdapAuthority.java | 9 +- .../ldap/userdetails/InetOrgPersonTests.java | 2 +- .../LdapUserDetailsServiceTests.java | 2 +- ...sServiceLdapAuthoritiesPopulatorTests.java | 2 +- ...MatcherDelegatingAuthorizationManager.java | 5 +- ...yContextPropagationChannelInterceptor.java | 164 - .../util/matcher/SimpMessageTypeMatcher.java | 3 +- ...extPropagationChannelInterceptorTests.java | 160 - .../handler/invocation/ResolvableMethod.java | 4 +- .../SimpDestinationMessageMatcherTests.java | 2 +- .../CsrfTokenHandshakeInterceptorTests.java | 2 +- .../JdbcOAuth2AuthorizedClientService.java | 3 +- ...ientOAuth2AuthorizationFailureHandler.java | 3 +- ...tiveOAuth2AuthorizationFailureHandler.java | 3 +- ...activeOAuth2AccessTokenResponseClient.java | 3 +- ...ethodValidatingRequestEntityConverter.java | 2 +- .../logout/LogoutTokenClaimAccessor.java | 96 - .../logout/LogoutTokenClaimNames.java | 70 - .../logout/OidcLogoutToken.java | 223 - .../InMemoryReactiveOidcSessionRegistry.java | 53 - .../session/ReactiveOidcSessionRegistry.java | 63 - .../session/InMemoryOidcSessionRegistry.java | 123 - .../oidc/session/OidcSessionInformation.java | 74 - .../oidc/session/OidcSessionRegistry.java | 59 - .../oidc/userinfo/OidcUserRequestUtils.java | 2 +- .../client/oidc/userinfo/OidcUserService.java | 2 +- ...dcClientInitiatedLogoutSuccessHandler.java | 4 +- .../SupplierClientRegistrationRepository.java | 65 - .../DefaultReactiveOAuth2UserService.java | 2 +- .../OAuth2AuthorizationCodeGrantFilter.java | 4 +- ...Auth2AuthorizedClientArgumentResolver.java | 6 +- ...uthorizedClientExchangeFilterFunction.java | 4 +- ...uthorizedClientExchangeFilterFunction.java | 4 +- ...odeReactiveAuthenticationManagerTests.java | 4 +- ...orizationCodeTokenResponseClientTests.java | 2 +- ...ntCredentialsTokenResponseClientTests.java | 2 +- ...uthenticationParametersConverterTests.java | 14 +- ...orizationCodeTokenResponseClientTests.java | 2 +- ...ntCredentialsTokenResponseClientTests.java | 4 +- .../OAuth2AuthenticationTokenMixinTests.java | 2 +- .../logout/TestOidcLogoutTokens.java | 61 - .../InMemoryOidcSessionRegistryTests.java | 103 - .../session/TestOidcSessionInformations.java | 45 - .../oidc/userinfo/OidcUserServiceTests.java | 4 +- .../ClientRegistrationsTests.java | 23 +- ...lierClientRegistrationRepositoryTests.java | 103 - .../DefaultOAuth2UserServiceTests.java | 4 +- ...DefaultReactiveOAuth2UserServiceTests.java | 4 +- ...izedClientExchangeFilterFunctionTests.java | 6 +- ...izedClientExchangeFilterFunctionTests.java | 4 +- .../oauth2/core/AuthorizationGrantType.java | 9 +- .../security/oauth2/core/ClaimAccessor.java | 10 +- .../core/ClientAuthenticationMethod.java | 5 - .../oauth2/core/OAuth2DeviceCode.java | 43 - .../security/oauth2/core/OAuth2UserCode.java | 43 - .../OAuth2AuthorizationManagers.java | 95 - .../OAuth2ReactiveAuthorizationManagers.java | 95 - .../OAuth2DeviceAuthorizationResponse.java | 242 -- .../core/endpoint/OAuth2ParameterNames.java | 34 +- ...orizationResponseHttpMessageConverter.java | 232 - .../OAuth2AuthorizationManagersTests.java | 76 - ...th2ReactiveAuthorizationManagersTests.java | 77 - .../converter/ClaimTypeConverterTests.java | 18 +- ...uth2AccessTokenResponseConverterTests.java | 111 +- ...2AccessTokenResponseMapConverterTests.java | 43 +- ...tionResponseHttpMessageConverterTests.java | 204 - .../core/oidc/OidcIdTokenBuilderTests.java | 6 +- .../core/oidc/OidcUserInfoBuilderTests.java | 6 +- .../oauth2/core/oidc/TestOidcIdTokens.java | 2 - .../oauth2/core/oidc/user/TestOidcUsers.java | 2 +- .../security/oauth2/jwt/JoseHeader.java | 4 +- .../JwtDecoderProviderConfigurationUtils.java | 15 +- .../security/oauth2/jwt/JwtDecoders.java | 9 +- .../security/oauth2/jwt/NimbusJwtDecoder.java | 60 +- .../oauth2/jwt/NimbusReactiveJwtDecoder.java | 74 +- ...eJwtDecoderProviderConfigurationUtils.java | 68 +- .../security/oauth2/jwt/JwtBuilderTests.java | 12 +- .../oauth2/jwt/JwtClaimValidatorTests.java | 2 +- .../jwt/MappedJwtClaimSetConverterTests.java | 58 +- .../oauth2/jwt/NimbusJweEncoderTests.java | 16 +- .../oauth2/jwt/NimbusJwtDecoderTests.java | 22 +- .../oauth2/jwt/NimbusJwtEncoderTests.java | 16 +- .../jwt/NimbusReactiveJwtDecoderTests.java | 43 +- ...ecoderProviderConfigurationUtilsTests.java | 322 -- .../JwtAuthenticationProvider.java | 7 +- .../JwtGrantedAuthoritiesConverter.java | 20 +- ...wtIssuerAuthenticationManagerResolver.java | 61 +- ...ReactiveAuthenticationManagerResolver.java | 63 +- .../OpaqueTokenAuthenticationProvider.java | 3 +- .../ReactiveJwtAuthenticationConverter.java | 21 +- .../NimbusOpaqueTokenIntrospector.java | 2 +- ...NimbusReactiveOpaqueTokenIntrospector.java | 2 +- .../BearerTokenAuthenticationEntryPoint.java | 3 +- .../web/DefaultBearerTokenResolver.java | 30 +- ...erTokenServerAuthenticationEntryPoint.java | 3 +- .../JwtAuthenticationProviderTests.java | 37 +- .../JwtGrantedAuthoritiesConverterTests.java | 16 +- ...icationManagerResolverDeprecatedTests.java | 257 -- ...uerAuthenticationManagerResolverTests.java | 69 +- ...icationManagerResolverDeprecatedTests.java | 261 -- ...iveAuthenticationManagerResolverTests.java | 66 +- ...activeJwtAuthenticationConverterTests.java | 46 +- ...sReactiveOpaqueTokenIntrospectorTests.java | 1 + ...gReactiveOpaqueTokenIntrospectorTests.java | 1 + .../web/DefaultBearerTokenResolverTests.java | 66 +- .../core/PayloadInterceptorRSocket.java | 4 +- .../rsocket/core/PayloadSocketAcceptor.java | 4 +- .../util/matcher/PayloadExchangeMatchers.java | 33 +- .../matcher/RoutePayloadExchangeMatcher.java | 4 +- ...AuthenticationPayloadInterceptorTests.java | 4 +- .../BasicAuthenticationDecoderTests.java | 2 +- ...ing-security-saml2-service-provider.gradle | 1 + .../AbstractSaml2AuthenticationRequest.java | 2 +- .../OpenSaml4AuthenticationProvider.java | 43 +- .../metadata/OpenSamlMetadataResolver.java | 69 +- ...equestMatcherMetadataResponseResolver.java | 181 - .../metadata/Saml2MetadataResolver.java | 4 - .../metadata/Saml2MetadataResponse.java | 38 - .../Saml2MetadataResponseResolver.java | 38 - ...oryRelyingPartyRegistrationRepository.java | 32 - .../OpenSamlAssertingPartyDetails.java | 88 +- ...etadataAssertingPartyDetailsConverter.java | 222 + ...dataRelyingPartyRegistrationConverter.java | 205 +- .../OpenSamlRelyingPartyRegistration.java | 180 - ...gistrationBuilderHttpMessageConverter.java | 6 +- .../RelyingPartyRegistration.java | 108 +- .../RelyingPartyRegistrationRepository.java | 13 - ...faultRelyingPartyRegistrationResolver.java | 78 +- .../OpenSamlAuthenticationTokenConverter.java | 316 -- ...PartyRegistrationPlaceholderResolvers.java | 134 - .../service/web/Saml2MetadataFilter.java | 128 +- ...penSaml4AuthenticationRequestResolver.java | 15 - ...OpenSamlAuthenticationRequestResolver.java | 18 +- .../Saml2WebSsoAuthenticationFilter.java | 24 +- .../OpenSaml4LogoutRequestResolver.java | 24 +- .../OpenSaml4LogoutResponseResolver.java | 12 +- .../logout/OpenSamlLogoutRequestResolver.java | 19 +- ...outRequestValidatorParametersResolver.java | 234 - .../OpenSamlLogoutResponseResolver.java | 19 +- .../logout/Saml2LogoutRequestFilter.java | 154 +- ...outRequestValidatorParametersResolver.java | 45 - .../logout/Saml2LogoutResponseFilter.java | 25 +- .../OpenSaml4AuthenticationProviderTests.java | 75 +- .../authentication/TestOpenSamlObjects.java | 27 +- .../TestSaml2Authentications.java | 40 - .../OpenSamlMetadataResolverTests.java | 25 - ...tMatcherMetadataResponseResolverTests.java | 136 - ...lyingPartyRegistrationRepositoryTests.java | 63 - ...taAssertingPartyDetailsConverterTests.java | 185 + ...elyingPartyRegistrationConverterTests.java | 151 - .../RelyingPartyRegistrationTests.java | 17 +- .../RelyingPartyRegistrationsTests.java | 2 +- ...SamlAuthenticationTokenConverterTests.java | 257 -- ...RegistrationPlaceholderResolversTests.java | 57 - .../service/web/Saml2MetadataFilterTests.java | 31 +- ...amlAuthenticationRequestResolverTests.java | 53 +- .../Saml2WebSsoAuthenticationFilterTests.java | 8 +- .../OpenSaml4LogoutRequestResolverTests.java | 58 +- .../OpenSamlLogoutRequestResolverTests.java | 2 +- ...questValidatorParametersResolverTests.java | 164 - .../OpenSamlLogoutResponseResolverTests.java | 2 +- settings.gradle | 3 +- .../src/main/resources/META-INF/security.tld | 2 +- test/spring-security-test.gradle | 1 + ...thAnonymousUserSecurityContextFactory.java | 2 +- .../WithMockUserSecurityContextFactory.java | 2 +- ...WithUserDetailsSecurityContextFactory.java | 3 +- ...yMockServerConfigurerOpaqueTokenTests.java | 2 +- ...SecurityMockServerConfigurersJwtTests.java | 4 +- ...yMockMvcRequestPostProcessorsJwtTests.java | 4 +- .../web/servlet/response/Gh3409Tests.java | 2 +- web/spring-security-web.gradle | 1 + .../security/web/DefaultRedirectStrategy.java | 30 +- .../security/web/FilterInvocation.java | 2 +- .../web/ObservationFilterChainDecorator.java | 57 +- .../access/ExceptionTranslationFilter.java | 2 +- .../web/access/NoOpAccessDeniedHandler.java | 41 - .../access/intercept/AuthorizationFilter.java | 20 +- .../web/access/intercept/RequestKey.java | 3 +- ...MatcherDelegatingAuthorizationManager.java | 148 +- ...AuthenticationTargetUrlRequestHandler.java | 51 +- .../NoOpAuthenticationEntryPoint.java | 42 - .../AbstractRememberMeServices.java | 10 +- .../RememberMeAuthenticationFilter.java | 2 +- .../TokenBasedRememberMeServices.java | 2 +- .../SwitchUserGrantedAuthority.java | 5 +- .../ui/DefaultLoginPageGeneratingFilter.java | 31 +- .../ui/DefaultLogoutPageGeneratingFilter.java | 4 +- .../www/BasicAuthenticationFilter.java | 43 +- .../www/DigestAuthenticationFilter.java | 2 +- .../web/csrf/CookieCsrfTokenRepository.java | 88 +- .../security/web/csrf/CsrfFilter.java | 8 +- .../XorCsrfTokenRequestAttributeHandler.java | 7 +- .../web/firewall/StrictHttpFirewall.java | 24 +- .../web/jaasapi/JaasApiIntegrationFilter.java | 3 +- .../web/savedrequest/CookieRequestCache.java | 2 +- .../web/savedrequest/SavedCookie.java | 28 +- .../SavedRequestAwareWrapper.java | 30 +- .../web/savedrequest/SimpleSavedRequest.java | 2 +- ...erverFormLoginAuthenticationConverter.java | 2 +- .../authentication/SwitchUserWebFilter.java | 3 +- .../csrf/CookieServerCsrfTokenRepository.java | 69 +- ...erverCsrfTokenRequestAttributeHandler.java | 7 +- .../ui/LoginPageGeneratingWebFilter.java | 4 +- .../ui/LogoutPageGeneratingWebFilter.java | 4 +- ...stedUrlRedirectInvalidSessionStrategy.java | 16 +- .../security/web/util/ThrowableAnalyzer.java | 2 +- .../web/util/matcher/AndRequestMatcher.java | 26 +- .../util/matcher/AntPathRequestMatcher.java | 3 +- .../matcher/DispatcherTypeRequestMatcher.java | 4 +- .../web/util/matcher/OrRequestMatcher.java | 21 +- .../web/util/matcher/RegexRequestMatcher.java | 4 +- .../web/util/matcher/RequestMatchers.java | 66 - .../web/DefaultRedirectStrategyTests.java | 21 +- .../ObservationFilterChainDecoratorTests.java | 25 - .../access/NoOpAccessDeniedHandlerTests.java | 41 - ...erDelegatingAuthorizationManagerTests.java | 282 +- ...nticationTargetUrlRequestHandlerTests.java | 111 - .../NoOpAuthenticationEntryPointTests.java | 41 - ...AuthenticatedAuthenticationTokenTests.java | 4 +- .../JdbcTokenRepositoryImplTests.java | 14 +- ...tentTokenBasedRememberMeServicesTests.java | 6 +- ...efaultLogoutPageGeneratingFilterTests.java | 4 +- .../www/BasicAuthenticationFilterTests.java | 57 - .../www/DigestAuthUtilsTests.java | 40 +- .../DigestAuthenticationEntryPointTests.java | 10 +- .../www/DigestAuthenticationFilterTests.java | 2 +- ...ecurityWebApplicationInitializerTests.java | 26 +- .../csrf/CookieCsrfTokenRepositoryTests.java | 201 +- ...CsrfTokenRequestAttributeHandlerTests.java | 10 +- .../CacheControlHeadersWriterTests.java | 2 +- .../header/writers/HstsHeaderWriterTests.java | 2 +- .../FrameOptionsHeaderWriterTests.java | 2 +- .../security/web/method/ResolvableMethod.java | 4 +- .../DefaultSavedRequestTests.java | 2 +- .../SavedRequestAwareWrapperTests.java | 21 +- .../context/ReactorContextWebFilterTests.java | 36 +- ...ontextServerWebExchangeWebFilterTests.java | 37 +- .../CookieServerCsrfTokenRepositoryTests.java | 127 +- ...CsrfTokenRequestAttributeHandlerTests.java | 14 +- .../CookieServerRequestCacheTests.java | 4 +- .../session/SessionManagementFilterTests.java | 70 +- .../web/util/TextEscapeUtilsTests.java | 4 +- .../util/matcher/AndRequestMatcherTests.java | 31 +- .../matcher/MediaTypeRequestMatcherTests.java | 2 +- .../util/matcher/OrRequestMatcherTests.java | 28 +- .../util/matcher/RequestMatchersTests.java | 86 - ...ngAuthenticationEntryPointTest-context.xml | 12 +- 676 files changed, 6306 insertions(+), 43249 deletions(-) delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/gradle-wrapper-upgrade-execution.yml delete mode 100644 cas/spring-security-cas.gradle delete mode 100644 cas/src/main/java/org/springframework/security/cas/SamlServiceProperties.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/ServiceProperties.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/authentication/CasAssertionAuthenticationToken.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/authentication/NullStatelessTicketCache.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCache.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/authentication/StatelessTicketCache.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/authentication/package-info.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/jackson2/AssertionImplMixin.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/jackson2/AttributePrincipalImplMixin.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixin.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/jackson2/CasJackson2Module.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/package-info.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/userdetails/AbstractCasAssertionUserDetailsService.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/userdetails/GrantedAuthorityFromAssertionAttributesUserDetailsService.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetails.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetails.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetailsSource.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/web/authentication/package-info.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/web/package-info.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/authentication/AbstractStatelessTicketCacheTests.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/authentication/NullStatelessTicketCacheTests.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCacheTests.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixinTests.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/userdetails/GrantedAuthorityFromAssertionAttributesUserDetailsServiceTests.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationEntryPointTests.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/web/ServicePropertiesTests.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetailsTests.java delete mode 100644 cas/src/test/resources/logback-test.xml delete mode 100644 cas/src/test/resources/org/springframework/security/cas/web/authentication/defaultserviceauthenticationdetails-explicit.xml delete mode 100644 cas/src/test/resources/org/springframework/security/cas/web/authentication/defaultserviceauthenticationdetails-passivity.xml delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherBuilderRegistry.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/AntPathRequestMatcherBuilder.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/DispatcherServletDelegatingRequestMatcherBuilder.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/MvcRequestMatcherBuilder.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuilder.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuilders.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletPatternRequestMatcher.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletRegistrationCollection.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/DefaultOidcLogoutTokenValidatorFactory.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthentication.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthenticationProvider.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutFilter.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandler.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutTokenValidator.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutAuthenticationConverter.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutAuthenticationToken.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java delete mode 100644 config/src/main/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrar.java delete mode 100644 config/src/main/java/org/springframework/security/config/web/server/DefaultOidcLogoutTokenValidatorFactory.java delete mode 100644 config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutAuthentication.java delete mode 100644 config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java delete mode 100644 config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.java delete mode 100644 config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutWebFilter.java delete mode 100644 config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandler.java delete mode 100644 config/src/main/java/org/springframework/security/config/web/server/OidcLogoutAuthenticationToken.java delete mode 100644 config/src/main/java/org/springframework/security/config/web/server/OidcLogoutServerAuthenticationConverter.java delete mode 100644 config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt delete mode 100644 config/src/main/kotlin/org/springframework/security/config/annotation/web/Saml2MetadataDsl.kt delete mode 100644 config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt delete mode 100644 config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt delete mode 100644 config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt delete mode 100644 config/src/main/resources/org/springframework/security/config/spring-security-6.1.rnc delete mode 100644 config/src/main/resources/org/springframework/security/config/spring-security-6.1.xsd delete mode 100644 config/src/main/resources/org/springframework/security/config/spring-security-6.2.rnc delete mode 100644 config/src/main/resources/org/springframework/security/config/spring-security-6.2.xsd delete mode 100644 config/src/test/java/org/springframework/security/config/TestMockHttpServletMappings.java delete mode 100644 config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableCustomMethodSecurity.java delete mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2AuthorizedClientManagerConfigurationTests.java delete mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherBuilderRegistryTests.java delete mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuildersTests.java delete mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletPatternRequestMatcherTests.java delete mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java delete mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java delete mode 100644 config/src/test/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests.java delete mode 100644 config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java delete mode 100644 config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt delete mode 100644 config/src/test/kotlin/org/springframework/security/config/annotation/web/Saml2MetadataDslTests.kt delete mode 100644 config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt delete mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-clients.xml delete mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-minimal.xml delete mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-providers.xml delete mode 100644 core/src/main/java/org/springframework/security/authorization/AuthoritiesAuthorizationManager.java delete mode 100644 core/src/test/java/org/springframework/security/DelegatingSecurityContextTestUtils.java delete mode 100644 core/src/test/java/org/springframework/security/authorization/AuthoritiesAuthorizationManagerTests.java delete mode 100644 core/src/test/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutorIntegrationTests.java delete mode 100644 core/src/test/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutorServiceIntegrationTests.java delete mode 100644 core/src/test/java/org/springframework/security/concurrent/DelegatingSecurityContextScheduledExecutorServiceIntegrationTests.java delete mode 100644 core/src/test/java/org/springframework/security/scheduling/DelegatingSecurityContextSchedulingTaskExecutorIntegrationTests.java delete mode 100644 core/src/test/java/org/springframework/security/scheduling/DelegatingSecurityContextTaskSchedulerIntegrationTests.java delete mode 100644 core/src/test/java/org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutorIntegrationTests.java delete mode 100644 core/src/test/java/org/springframework/security/task/DelegatingSecurityContextTaskExecutorIntegrationTests.java delete mode 100644 docs/modules/ROOT/assets/images/servlet/authorization/methodsecurity.odg delete mode 100644 docs/modules/ROOT/assets/images/servlet/authorization/methodsecurity.png delete mode 100644 docs/modules/ROOT/pages/migration-7/configuration.adoc delete mode 100644 docs/modules/ROOT/pages/migration-7/index.adoc delete mode 100644 docs/modules/ROOT/pages/migration-7/ldap.adoc delete mode 100644 docs/modules/ROOT/pages/migration/authorization.adoc create mode 100644 docs/modules/ROOT/pages/migration/reactive.adoc create mode 100644 docs/modules/ROOT/pages/migration/servlet/authentication.adoc create mode 100644 docs/modules/ROOT/pages/migration/servlet/authorization.adoc create mode 100644 docs/modules/ROOT/pages/migration/servlet/exploits.adoc create mode 100644 docs/modules/ROOT/pages/migration/servlet/index.adoc create mode 100644 docs/modules/ROOT/pages/migration/servlet/session-management.adoc delete mode 100644 docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc delete mode 100644 docs/modules/ROOT/pages/servlet/authentication/cas.adoc create mode 100644 docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc create mode 100644 docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc create mode 100644 docs/modules/ROOT/pages/servlet/authorization/secure-objects.adoc delete mode 100644 docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc delete mode 100644 git/hooks/.forward-merge.swp delete mode 100644 messaging/src/main/java/org/springframework/security/messaging/context/SecurityContextPropagationChannelInterceptor.java delete mode 100644 messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextPropagationChannelInterceptorTests.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/LogoutTokenClaimAccessor.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/LogoutTokenClaimNames.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcLogoutToken.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/server/session/InMemoryReactiveOidcSessionRegistry.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/server/session/ReactiveOidcSessionRegistry.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistry.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/OidcSessionInformation.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/OidcSessionRegistry.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/SupplierClientRegistrationRepository.java delete mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/logout/TestOidcLogoutTokens.java delete mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistryTests.java delete mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/TestOidcSessionInformations.java delete mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/SupplierClientRegistrationRepositoryTests.java delete mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2DeviceCode.java delete mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2UserCode.java delete mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagers.java delete mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2ReactiveAuthorizationManagers.java delete mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2DeviceAuthorizationResponse.java delete mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverter.java delete mode 100644 oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagersTests.java delete mode 100644 oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2ReactiveAuthorizationManagersTests.java delete mode 100644 oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverterTests.java delete mode 100644 oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java delete mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverDeprecatedTests.java delete mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverDeprecatedTests.java delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/RequestMatcherMetadataResponseResolver.java delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResponse.java delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResponseResolver.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverter.java delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/OpenSamlAuthenticationTokenConverter.java delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationPlaceholderResolvers.java delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolver.java delete mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestValidatorParametersResolver.java delete mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2Authentications.java delete mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/RequestMatcherMetadataResponseResolverTests.java delete mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepositoryTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverterTests.java delete mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/OpenSamlAuthenticationTokenConverterTests.java delete mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationPlaceholderResolversTests.java delete mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolverTests.java delete mode 100644 web/src/main/java/org/springframework/security/web/access/NoOpAccessDeniedHandler.java delete mode 100644 web/src/main/java/org/springframework/security/web/authentication/NoOpAuthenticationEntryPoint.java delete mode 100644 web/src/main/java/org/springframework/security/web/util/matcher/RequestMatchers.java delete mode 100644 web/src/test/java/org/springframework/security/web/access/NoOpAccessDeniedHandlerTests.java delete mode 100644 web/src/test/java/org/springframework/security/web/authentication/AbstractAuthenticationTargetUrlRequestHandlerTests.java delete mode 100644 web/src/test/java/org/springframework/security/web/authentication/NoOpAuthenticationEntryPointTests.java delete mode 100644 web/src/test/java/org/springframework/security/web/util/matcher/RequestMatchersTests.java diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 596cf8f9ff..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,92 +0,0 @@ -version: 2 - -registries: - spring-milestones: - type: maven-repository - url: https://repo.spring.io/milestone - -updates: - - - package-ecosystem: "gradle" - target-branch: "main" - milestone: 319 # 6.2.x - directory: "/" - schedule: - interval: "daily" - time: "03:00" - timezone: "Etc/UTC" - labels: [ "type: dependency-upgrade" ] - registries: - - "spring-milestones" - ignore: - - dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency - - dependency-name: "org.python:jython" # jython updates break integration tests - - dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes - - dependency-name: "org.junit:junit-bom" - update-types: [ "version-update:semver-major" ] - - dependency-name: "org.mockito:mockito-bom" - update-types: [ "version-update:semver-major" ] - - dependency-name: "*" - update-types: [ "version-update:semver-major", "version-update:semver-minor" ] - - - package-ecosystem: "gradle" - target-branch: "6.1.x" - milestone: 318 # 6.1.x - directory: "/" - schedule: - interval: "daily" - time: "03:00" - timezone: "Etc/UTC" - labels: [ "type: dependency-upgrade" ] - ignore: - - dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency - - dependency-name: "org.python:jython" # jython updates break integration tests - - dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes - - dependency-name: "org.junit:junit-bom" - update-types: [ "version-update:semver-major" ] - - dependency-name: "org.mockito:mockito-bom" - update-types: [ "version-update:semver-major" ] - - dependency-name: "*" - update-types: [ "version-update:semver-major", "version-update:semver-minor" ] - - - package-ecosystem: "gradle" - target-branch: "6.0.x" - milestone: 143 # 6.0.x - directory: "/" - schedule: - interval: "daily" - time: "03:00" - timezone: "Etc/UTC" - labels: [ "type: dependency-upgrade" ] - ignore: - - dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency - - dependency-name: "org.python:jython" # jython updates break integration tests - - dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes - - dependency-name: "org.junit:junit-bom" - update-types: [ "version-update:semver-major" ] - - dependency-name: "org.mockito:mockito-bom" - update-types: [ "version-update:semver-major" ] - - dependency-name: "*" - update-types: [ "version-update:semver-major", "version-update:semver-minor" ] - - - package-ecosystem: "gradle" - target-branch: "5.8.x" - milestone: 246 # 5.8.x - directory: "/" - schedule: - interval: "daily" - time: "03:00" - timezone: "Etc/UTC" - labels: [ "type: dependency-upgrade" ] - ignore: - - dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency - - dependency-name: "org.python:jython" # jython updates break integration tests - - dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes - - dependency-name: "io.mockk:mockk" # mockk updates break tests - - dependency-name: "org.opensaml:*" # org.opensaml maintains two different versions, so it must be updated manually - - dependency-name: "org.junit:junit-bom" - update-types: [ "version-update:semver-major" ] - - dependency-name: "org.mockito:mockito-bom" - update-types: [ "version-update:semver-major" ] - - dependency-name: "*" - update-types: [ "version-update:semver-major", "version-update:semver-minor" ] diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index ab8f3dbdd8..1255f25028 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -69,13 +69,6 @@ jobs: snapshot_tests: name: Test against snapshots needs: [prerequisites] - strategy: - matrix: - include: - - java-version: '21-ea' - toolchain: '21' - - java-version: '17' - toolchain: '17' runs-on: ubuntu-latest if: needs.prerequisites.outputs.runjobs steps: @@ -83,14 +76,14 @@ jobs: - name: Set up gradle uses: spring-io/spring-gradle-build-action@v1 with: - java-version: ${{ matrix.java-version }} + java-version: '17' distribution: 'temurin' - name: Snapshot Tests run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" - ./gradlew test --refresh-dependencies -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PtestToolchain=${{ matrix.toolchain }} -PspringFrameworkVersion='6.1.+' -PreactorVersion='2023.0.+' -PspringDataVersion='2023.1.+' -PlocksDisabled --stacktrace + ./gradlew test --refresh-dependencies -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PspringFrameworkVersion='6.0.+' -PreactorVersion='2022.0.+' -PspringDataVersion='2022.0.+' -PlocksDisabled --stacktrace check_samples: name: Check Samples project needs: [prerequisites] diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 24af311b5b..73c3e34ffe 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -7,8 +7,8 @@ on: tags: '**' repository_dispatch: types: request-build-reference # legacy - #schedule: - #- cron: '0 10 * * *' # Once per day at 10am UTC + schedule: + - cron: '0 10 * * *' # Once per day at 10am UTC workflow_dispatch: permissions: read-all jobs: diff --git a/.github/workflows/gradle-wrapper-upgrade-execution.yml b/.github/workflows/gradle-wrapper-upgrade-execution.yml deleted file mode 100644 index dcb48ab942..0000000000 --- a/.github/workflows/gradle-wrapper-upgrade-execution.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Execute Gradle Wrapper Upgrade - -on: - schedule: - - cron: '0 2 * * *' # 2am UTC - workflow_dispatch: - -jobs: - upgrade_wrapper: - name: Execution - runs-on: ubuntu-latest - steps: - - name: Set up Git configuration - env: - TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git config --global url."https://unused-username:${TOKEN}@github.com/".insteadOf "https://github.com/" - git config --global user.name 'github-actions[bot]' - git config --global user.email 'github-actions[bot]@users.noreply.github.com' - - name: Checkout - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - - name: Set up Gradle - uses: gradle/gradle-build-action@v2 - - name: Upgrade Wrappers - run: ./gradlew clean upgradeGradleWrapperAll --continue -Porg.gradle.java.installations.auto-download=false - env: - WRAPPER_UPGRADE_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-scheduler.yml b/.github/workflows/release-scheduler.yml index c3a37c30ce..e8c0971d22 100644 --- a/.github/workflows/release-scheduler.yml +++ b/.github/workflows/release-scheduler.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: # List of active maintenance branches. - branch: [ main, 6.1.x, 6.0.x, 5.8.x ] + branch: [ main, 6.0.x, 5.8.x, 5.7.x, 5.6.x ] runs-on: ubuntu-latest steps: - name: Checkout diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc index ad08a4116b..7af68ef805 100644 --- a/CONTRIBUTING.adoc +++ b/CONTRIBUTING.adoc @@ -1,154 +1,232 @@ -= Contributing to Spring Security +_Have something you'd like to contribute to the framework? We welcome pull requests, but ask that you carefully read this document first to understand how best to submit them; what kind of changes are likely to be accepted; and what to expect from the Spring Security team when evaluating your submission._ -First off, thank you for taking the time to contribute! :+1: :tada: +_Please refer back to this document as a checklist before issuing any pull request; this will save time for everyone!_ -== Table of Contents += Code of Conduct -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> +Please see our https://github.com/spring-projects/.github/blob/main/CODE_OF_CONDUCT.md[code of conduct]. -[[code-of-conduct]] -== Code of Conduct += Similar but different -This project is governed by the https://github.com/spring-projects/.github/blob/main/CODE_OF_CONDUCT.md[Spring code of conduct]. -By participating you are expected to uphold this code. -Please report unacceptable behavior to spring-code-of-conduct@pivotal.io. +Each Spring module is slightly different from one another in terms of team size, number of issues, etc. Therefore, each project is managed slightly different. You will notice that this document is very similar to the https://github.com/spring-projects/spring-framework/wiki/Contributor-guidelines[Spring Framework Contributor guidelines]. However, there are some subtle differences between the two documents, so please be sure to read this document thoroughly. -[[how-to-contribute]] -== How to Contribute += Importing into IDE -[[ask-questions]] -=== Ask Questions +The following provides information on setting up a development environment that can run the sample in https://www.springsource.org/sts[Spring Tool Suite 3.6.0+]. Other IDE's should work using Gradle's IDE support, but have not been tested. -If you have a question, check Stack Overflow using -https://stackoverflow.com/questions/tagged/spring-security+or+spring-ldap+or+spring-authorization-server+or+spring-session?tab=Newest[this list of tags]. -Find an existing discussion, or start a new one if necessary. +* IDE Setup +** Install Spring Tool Suite 3.6.0+ +** You will need the following plugins installed (can be found on the Extensions Page) +*** Gradle Eclipse +*** Groovy Eclipse +* Importing the project into Spring Tool Suite +** File -> Import… -> Gradle Project -If you believe there is an issue, search through https://github.com/spring-projects/spring-security/issues[existing issues] trying a few different ways to find discussions, past or current, that are related to the issue. -Reading those discussions helps you to learn about the issue, and helps us to make a decision. +As of new versions of Spring Tool Suite, you might need to install Groovy Eclipse pointing directly to the updated plugin location. To install Groovy Eclipse on Spring Tool Suite based on Eclipse Oxigen you must do the following steps: -[[find-an-issue]] -=== Find an Existing Issue +Help -> Install New Software… -> Add the following URL into _Work with_ field: +https://dist.springsource.org/snapshot/GRECLIPSE/e4.7/[https://dist.springsource.org/snapshot/GRECLIPSE/e4.7/] -There are many issues in Spring Security with the labels https://github.com/spring-projects/spring-security/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+ideal-for-contribution%22[`ideal-for-contribution`] or https://github.com/spring-projects/spring-security/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+first-timers-only%22[`first-timers-only`] that are a great way to contribute to a discussion or <>. -You can volunteer by commenting on these tickets, and we will assign them to you. += Understand the basics -[[create-an-issue]] -=== Create an Issue +Not sure what a pull request is, or how to submit one? Take a look at GitHub's excellent https://help.github.com/articles/using-pull-requests[help documentation first]. -Reporting an issue or making a feature request is a great way to contribute. -Your feedback and the conversations that result from it provide a continuous flow of ideas. -However, before creating a ticket, please take the time to <> first. += Search GitHub issues; create an issue if necessary -If you create an issue after a discussion on Stack Overflow, please provide a description in the issue instead of simply referring to Stack Overflow. -The issue tracker is an important place of record for design discussions and should be self-sufficient. +Is there already an issue that addresses your concern? Do a bit of searching in our https://github.com/spring-projects/spring-security/issues[GitHub issues] to see if you can find something similar. If not, please create a new issue before submitting a pull request unless the change is not a user facing issue. -Once you're ready, create an issue on https://github.com/spring-projects/spring-security/issues[GitHub]. += Discuss non-trivial contribution ideas with committers -Many issues are caused by subtle behavior, typos, and unintended configuration. -Creating a https://stackoverflow.com/help/minimal-reproducible-example[Minimal Reproducible Example] (starting with https://start.spring.io for example) of the problem helps the team quickly triage your issue and get to the core of the problem. +If you're considering anything more than correcting a typo or fixing a minor bug, please discuss it on the https://gitter.im/spring-projects/spring-security[Spring Security Gitter] before submitting a pull request. We're happy to provide guidance but please spend an hour or two researching the subject on your own including searching the forums for prior discussions. -We love contributors, and we may ask you to <>. += Sign the Contributor License Agreement -[[issue-lifecycle]] -=== Issue Lifecycle +If you have not previously done so, please fill out and submit the https://cla.pivotal.io/sign/spring[Contributor License Agreement]. -When an issue is first created, it is flagged `waiting-for-triage` waiting for a team member to triage it. -Once the issue has been reviewed, the team may ask for further information if needed, and based on the findings, the issue is either assigned a target branch (or no branch if a feature) or is closed with a specific status. -The target branch is https://spring.io/projects/spring-security#support[the earliest supported branch] where <>. += Create your branch from oldest maintenance branch -When a fix is ready, the issue is closed and may still be re-opened until the fix is released. -After that the issue will typically no longer be reopened. -In rare cases if the issue was not at all fixed, the issue may be re-opened. -In most cases however any follow-up reports will need to be created as new issues with a fresh description. +Create your topic branch to be submitted as a pull request from the oldest impacted and supported maintenance branch. +You can find the supported versions by looking at the https://github.com/spring-projects/spring-security/milestones[milestones page]. +Switch to a branch named `..x` from the smallest milestone in the format of `..(-)`. +The spring team will ensure the code gets merged forward into additional branches. -[[build-from-source]] -=== Build from Source += Use short branch names -See https://github.com/spring-projects/spring-security/tree/main#building-from-source[Build from Source] for instructions on how to check out, build, and import the Spring Security source code into your IDE. +Branches used when submitting pull requests should preferably be named according to GitHub issues, e.g. `gh-1234` or `gh-1234-fix-npe`. Otherwise, use succinct, lower-case, dash (`-`) delimited names, such as `fix-warnings` or `fix-typo`. This is important, because branch names show up in the merge commits that result from accepting pull requests, and should be as expressive and concise as possible. -[[code-style]] -=== Source Code Style += Keep commits focused -The wiki pages https://github.com/spring-projects/spring-framework/wiki/Code-Style[Code Style] and https://github.com/spring-projects/spring-framework/wiki/IntelliJ-IDEA-Editor-Settings[IntelliJ IDEA Editor Settings] define the source file coding standards we use along with some IDEA editor settings we customize. +Remember each ticket should be focused on a single item of interest since the tickets are used to produce the changelog. Since each commit should be tied to a single GitHub issue, ensure that your commits are focused. For example, do not include an update to a transitive library in your commit unless the GitHub is to update the library. Reviewing your commits is essential before sending a pull request. -To format the code as well as check the style, run `./gradle format check`. += Mind the whitespace -[[submit-a-pull-request]] -=== Submit a Pull Request +Please carefully follow the whitespace and formatting conventions already present in the framework. -We are excited for your pull request! :heart: +. Tabs, not spaces +. Unix (LF), not dos (CRLF) line endings +. Eliminate all trailing whitespace +. Aim to wrap code at 120 characters, but favor readability over wrapping +. Preserve existing formatting; i.e. do not reformat code for its own sake +. Search the codebase using `git grep` and other tools to discover common naming conventions, etc. +. UTF-8 encoding for Java sources and XML files -Please do your best to follow these steps. -Don't worry if you don't get them all correct the first time, we will help you. +Whitespace management tips + +. You can use the https://marketplace.eclipse.org/content/anyedit-tools[AnyEdit Eclipse plugin] to ensure spaces are used and to clean up trailing whitespaces. +. Use Git's `pre-commit.sample` hook to prevent invalid whitespace from being pushed out. You can enable it by moving `.git/hooks/pre-commit.sample` to `.git/hooks/pre-commit` and ensuring it is executable. For more information on hooks refer to https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks[https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks]. + += Add Apache license header to all new classes -[[sign-cla]] -1. If you have not previously done so, please sign the https://cla.spring.io/sign/spring[Contributor License Agreement]. -You will be reminded automatically when you submit the PR. -[[create-an-issue]] -1. Must you https://github.com/spring-projects/spring-security/issues/new/choose[create an issue] first? No, but it is recommended for features and larger bug fixes. It's easier discuss with the team first to determine the right fix or enhancement. -For typos and straightforward bug fixes, starting with a pull request is encouraged. -Please include a description for context and motivation. -Note that the team may close your pull request if it's not a fit for the project. -[[choose-a-branch]] -1. Always check out the branch indicated in the milestone and submit pull requests against it (for example, for milestone `5.8.3` use the `5.8.x` branch). -If there is no milestone, choose `main`. -Once merged, the fix will be forwarded-ported to applicable branches including `main`. -[[create-a-local-branch]] -1. Create a local branch -If this is for an issue, consider a branch name with the issue number, like `gh-22276`. -[[write-tests]] -1. Add JUnit Tests for your changes -[[update-copyright]] -1. In all files you edited, if the copyright header is of the form 2002-20xx, update the final copyright year to the current year. -[[add-since]] -1. If on `main`, add `@since` JavaDoc attributes to new public APIs that your PR adds -[[change-rnc]] -1. If you are updating the XSD, please instead update the RNC file and then run `./gradlew :spring-security-config:rncToXsd`. -[[format-code]] -1. For each commit, build the code using `./gradlew format check`. -This command ensures the code meets most of <>; a notable exception is import order. -[[commit-atomically]] -1. Choose the granularity of your commits consciously and squash commits that represent -multiple edits or corrections of the same logical change. -See https://git-scm.com/book/en/Git-Tools-Rewriting-History[Rewriting History section of Pro Git] for an overview of streamlining the commit history. -[[format-commit-messages]] -1. Format commit messages using 55 characters for the subject line, 72 characters per line -for the description, followed by the issue fixed, for example, `Closes gh-22276`. -See the https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project#Commit-Guidelines[Commit Guidelines section of Pro Git] for best practices around commit messages, and use `git log` to see some examples. -Present tense is preferred. -+ -[indent=0] ---- -Address NullPointerException +/* + * 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. + */ -Closes gh-22276 ----- -[[reference-issue]] -1. If there is a prior issue, reference the GitHub issue number in the description of the pull request. -+ -[indent=0] ----- -Closes gh-22276 +package ...; ---- -If accepted, your contribution may be heavily modified as needed prior to merging. -You will likely retain author attribution for your Git commits granted that the bulk of your changes remain intact. -You may also be asked to rework the submission. += Update Apache license header to modified files as necessary -If asked to make corrections, simply push the changes against the same branch, and your pull request will be updated. -In other words, you do not need to create a new pull request when asked to make changes. -When it is time to merge, you'll be asked to squash your commits. +Always check the date range in the license header. For example, if you've modified a file in 2020 whose header still reads -==== Participate in Reviews +---- + * Copyright 2002-2012 the original author or authors. +---- -Helping to review pull requests is another great way to contribute. -Your feedback can help to shape the implementation of new features. -When reviewing pull requests, however, please refrain from approving or rejecting a PR unless you are a core committer for Spring Security. +then be sure to update it to the current year appropriately (e.g. 2020) + +---- + * Copyright 2002-2020 the original author or authors. +---- + += Use @since tags for newly-added public API types and methods + +Example: + +---- +/** + * … + * + * @author First Last + * @since 5.4 + * @see … + */ +---- + += Submit JUnit test cases for all behavior changes + +Search the codebase to find related unit tests and add additional `@Test` methods within. + +. Any new tests should end in the name `Tests` (note this is plural). For example, a valid name would be `FilterChainProxyTests`. An invalid name would be `FilterChainProxyTest`. +. New test methods should not start with test. This is an old JUnit3 convention and is not necessary since the method is annotated with `@Test`. + += Update spring-security-x.y.rnc for schema changes + +Update the https://www.relaxng.org[RELAX NG] schema `spring-security-x.y.rnc` instead of `spring-security-x.y.xsd` if you contribute changes to supported XML configuration. The XML schema file can be generated the following Gradle task: + +---- +./gradlew :spring-security-config:rncToXsd +---- + +Changes to the XML schema will be overwritten by the Gradle build task. + += Squash commits + +Use `git rebase --interactive`, `git add --patch` and other tools to "squash" multiple commits into atomic changes. In addition to the man pages for `git`, there are https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History[many resources online] to help you understand how these tools work. + += Use real name in git commits + +Please configure Git to use your real first and last name for any commits you intend to submit as pull requests. Make sure the name is properly capitalized as submitted to the https://cla.pivotal.io[Pivotal Contributor License Agreement]: + +---- +First Last +---- + +This helps ensure traceability against the CLA, and also goes a long way to ensuring useful output from tools like Git shortlog and others. + +You can configure this globally: + +---- +git config --global user.name "First Last" +git config --global user.email user@example.com +---- + +or locally for the current repository by omitting the `--global` flag: + +---- +git config user.name "First Last" +git config user.email user@example.com +---- + += Format commit messages + +. Keep the subject line to 50 characters or less if possible +. Do not end the subject line with a period +. In the body of the commit message, explain how things worked before this commit, what has changed, and how things work now +. Include `Closes gh-` at the end if this fixes a GitHub issue +. Avoid markdown, including back-ticks identifying code + +Example: + +---- +Short (50 chars or less) summary of changes + +More detailed explanatory text, if necessary. Wrap it to about 72 +characters or so. In some contexts, the first line is treated as the +subject of an email and the rest of the text as the body. The blank +line separating the summary from the body is critical (unless you omit +the body entirely); tools like rebase can get confused if you run the +two together. + +Further paragraphs come after blank lines. + + - Bullet points are okay, too + + - Typically a hyphen or asterisk is used for the bullet, preceded by a + single space, with blank lines in between, but conventions vary here + +Closes gh-123 +---- + + += Run all tests prior to submission + +---- +./gradlew clean build integrationTest +---- + += Submit your pull request + +*Subject line:* + +Follow the same conventions for pull request subject lines as mentioned above for commit message subject lines. + +*In the body:* + +. Explain your use case. What led you to submit this change? Why were existing mechanisms in the framework insufficient? Make a case that this is a general-purpose problem and that yours is a general-purpose solution, etc +. Add any additional information and ask questions; start a conversation, or continue one from GitHub Issues +. Mention any GitHub Issues +. Also mention that you have submitted the CLA as described above +Note that for pull requests containing a single commit, GitHub will default the subject line and body of the pull request to match the subject line and body of the commit message. This is fine, but please also include the items above in the body of the request. + += Mention your pull request on the associated GitHub issue + +Add a comment to the associated GitHub issue(s) linking to your new pull request. + += Expect discussion and rework + +The Spring team takes a very conservative approach to accepting contributions to the framework. This is to keep code quality and stability as high as possible, and to keep complexity at a minimum. Your changes, if accepted, may be heavily modified prior to merging. You will retain "Author:" attribution for your Git commits granted that the bulk of your changes remain intact. You may be asked to rework the submission for style (as explained above) and/or substance. Again, we strongly recommend discussing any serious submissions with the Spring Framework team prior to engaging in serious development work. + +Note that you can always force push (`git push -f`) reworked / rebased commits against the branch used to submit your pull request. i.e. you do not need to issue a new pull request when asked to make changes. diff --git a/RELEASE.adoc b/RELEASE.adoc index 3e9f930ca0..16c16b8fd6 100644 --- a/RELEASE.adoc +++ b/RELEASE.adoc @@ -59,9 +59,9 @@ The scheduled release process currently runs every Monday but only releases when The automated release process occurs on the following branches: * `main` -* `6.0.x` * `5.8.x` * `5.7.x` +* `5.6.x` For each of the above branches, the automated process performs the following checks before proceeding with the release: diff --git a/acl/src/main/java/org/springframework/security/acls/AclPermissionEvaluator.java b/acl/src/main/java/org/springframework/security/acls/AclPermissionEvaluator.java index 53fe8c1efe..15907a22ca 100644 --- a/acl/src/main/java/org/springframework/security/acls/AclPermissionEvaluator.java +++ b/acl/src/main/java/org/springframework/security/acls/AclPermissionEvaluator.java @@ -118,7 +118,8 @@ public class AclPermissionEvaluator implements PermissionEvaluator { if (permission instanceof Permission[]) { return Arrays.asList((Permission[]) permission); } - if (permission instanceof String permString) { + if (permission instanceof String) { + String permString = (String) permission; Permission p = buildPermission(permString); if (p != null) { return Arrays.asList(p); diff --git a/acl/src/main/java/org/springframework/security/acls/domain/AbstractPermission.java b/acl/src/main/java/org/springframework/security/acls/domain/AbstractPermission.java index 8365552a5a..147f643966 100644 --- a/acl/src/main/java/org/springframework/security/acls/domain/AbstractPermission.java +++ b/acl/src/main/java/org/springframework/security/acls/domain/AbstractPermission.java @@ -56,9 +56,10 @@ public abstract class AbstractPermission implements Permission { if (obj == null) { return false; } - if (!(obj instanceof Permission other)) { + if (!(obj instanceof Permission)) { return false; } + Permission other = (Permission) obj; return (this.mask == other.getMask()); } diff --git a/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcAclServiceTests.java b/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcAclServiceTests.java index 6253c29702..b817d33c1e 100644 --- a/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcAclServiceTests.java +++ b/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcAclServiceTests.java @@ -112,7 +112,7 @@ public class JdbcAclServiceTests { given(this.jdbcOperations.query(anyString(), eq(args), any(RowMapper.class))).willReturn(result); ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 1L); List objectIdentities = this.aclService.findChildren(objectIdentity); - assertThat(objectIdentities).hasSize(1); + assertThat(objectIdentities.size()).isEqualTo(1); assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("5577"); } @@ -127,7 +127,7 @@ public class JdbcAclServiceTests { public void findChildrenWithoutIdType() { ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 4711L); List objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity); - assertThat(objectIdentities).hasSize(1); + assertThat(objectIdentities.size()).isEqualTo(1); assertThat(objectIdentities.get(0).getType()).isEqualTo(MockUntypedIdDomainObject.class.getName()); assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(5000L); } @@ -143,7 +143,7 @@ public class JdbcAclServiceTests { public void findChildrenOfIdTypeLong() { ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US-PAL"); List objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity); - assertThat(objectIdentities).hasSize(2); + assertThat(objectIdentities.size()).isEqualTo(2); assertThat(objectIdentities.get(0).getType()).isEqualTo(MockLongIdDomainObject.class.getName()); assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(4711L); assertThat(objectIdentities.get(1).getType()).isEqualTo(MockLongIdDomainObject.class.getName()); @@ -155,7 +155,7 @@ public class JdbcAclServiceTests { ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US"); this.aclServiceIntegration.setAclClassIdSupported(true); List objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity); - assertThat(objectIdentities).hasSize(1); + assertThat(objectIdentities.size()).isEqualTo(1); assertThat(objectIdentities.get(0).getType()).isEqualTo("location"); assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("US-PAL"); } @@ -165,7 +165,7 @@ public class JdbcAclServiceTests { ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockUntypedIdDomainObject.class, 5000L); this.aclServiceIntegration.setAclClassIdSupported(true); List objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity); - assertThat(objectIdentities).hasSize(1); + assertThat(objectIdentities.size()).isEqualTo(1); assertThat(objectIdentities.get(0).getType()).isEqualTo("costcenter"); assertThat(objectIdentities.get(0).getIdentifier()) .isEqualTo(UUID.fromString("25d93b3f-c3aa-4814-9d5e-c7c96ced7762")); @@ -186,7 +186,7 @@ public class JdbcAclServiceTests { ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US"); this.aclServiceIntegration.setAclClassIdSupported(true); List objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity); - assertThat(objectIdentities).hasSize(1); + assertThat(objectIdentities.size()).isEqualTo(1); assertThat(objectIdentities.get(0).getType()).isEqualTo("location"); assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("prefix:US-PAL"); } diff --git a/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcMutableAclServiceTests.java b/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcMutableAclServiceTests.java index 76e5a10ec9..f7f69d19e4 100644 --- a/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcMutableAclServiceTests.java +++ b/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcMutableAclServiceTests.java @@ -454,7 +454,7 @@ public class JdbcMutableAclServiceTests { CustomSid customSid = new CustomSid("Custom sid"); given(customJdbcMutableAclService.createOrRetrieveSidPrimaryKey("Custom sid", false, false)).willReturn(1L); Long result = customJdbcMutableAclService.createOrRetrieveSidPrimaryKey(customSid, false); - assertThat(Long.valueOf(1L)).isEqualTo(result); + assertThat(new Long(1L)).isEqualTo(result); } protected Authentication getAuth() { diff --git a/acl/src/test/java/org/springframework/security/acls/sid/SidTests.java b/acl/src/test/java/org/springframework/security/acls/sid/SidTests.java index 3b002190bc..d52d6980fd 100644 --- a/acl/src/test/java/org/springframework/security/acls/sid/SidTests.java +++ b/acl/src/test/java/org/springframework/security/acls/sid/SidTests.java @@ -120,9 +120,9 @@ public class SidTests { PrincipalSid principalSid = new PrincipalSid(authentication); GrantedAuthority ga = new SimpleGrantedAuthority("ROLE_TEST"); GrantedAuthoritySid gaSid = new GrantedAuthoritySid(ga); - assertThat("johndoe").isEqualTo(principalSid.getPrincipal()); + assertThat("johndoe".equals(principalSid.getPrincipal())).isTrue(); assertThat("scott".equals(principalSid.getPrincipal())).isFalse(); - assertThat("ROLE_TEST").isEqualTo(gaSid.getGrantedAuthority()); + assertThat("ROLE_TEST".equals(gaSid.getGrantedAuthority())).isTrue(); assertThat("ROLE_TEST2".equals(gaSid.getGrantedAuthority())).isFalse(); } diff --git a/aspects/spring-security-aspects.gradle b/aspects/spring-security-aspects.gradle index 4323430c3d..eb43bb8757 100644 --- a/aspects/spring-security-aspects.gradle +++ b/aspects/spring-security-aspects.gradle @@ -29,4 +29,10 @@ dependencies { testAspect sourceSets.main.output } +sourceSets.main.aspectj.srcDir "src/main/java" +sourceSets.main.java.srcDirs = files() + +sourceSets.test.aspectj.srcDir "src/test/java" +sourceSets.test.java.srcDirs = files() + compileAspectj.ajcOptions.outxmlfile = "META-INF/aop.xml" diff --git a/aspects/src/test/java/org/springframework/security/access/intercept/aspectj/aspect/AnnotationSecurityAspectTests.java b/aspects/src/test/java/org/springframework/security/access/intercept/aspectj/aspect/AnnotationSecurityAspectTests.java index 2e0f4ceea1..778268a5a3 100644 --- a/aspects/src/test/java/org/springframework/security/access/intercept/aspectj/aspect/AnnotationSecurityAspectTests.java +++ b/aspects/src/test/java/org/springframework/security/access/intercept/aspectj/aspect/AnnotationSecurityAspectTests.java @@ -143,8 +143,8 @@ public class AnnotationSecurityAspectTests { SecurityContextHolder.getContext().setAuthentication(this.anne); List objects = this.prePostSecured.postFilterMethod(); assertThat(objects).hasSize(2); - assertThat(objects).contains("apple"); - assertThat(objects).contains("aubergine"); + assertThat(objects.contains("apple")).isTrue(); + assertThat(objects.contains("aubergine")).isTrue(); } private void configureForElAnnotations() { diff --git a/build.gradle b/build.gradle index d2af627741..a169da4db5 100644 --- a/build.gradle +++ b/build.gradle @@ -14,10 +14,6 @@ buildscript { } } -plugins { - alias(libs.plugins.org.gradle.wrapper.upgrade) -} - apply plugin: 'io.spring.nohttp' apply plugin: 'locks' apply plugin: 's101' @@ -39,7 +35,6 @@ ext.milestoneBuild = !(snapshotBuild || releaseBuild) repositories { mavenCentral() - maven { url "https://repo.spring.io/milestone" } } tasks.named("saganCreateRelease") { @@ -91,31 +86,17 @@ tasks.named("dispatchGitHubWorkflow") { } } -def toolchainVersion() { - if (project.hasProperty('testToolchain')) { - return project.property('testToolchain').toString().toInteger() - } - return 17 -} - subprojects { - java { - toolchain { - languageVersion = JavaLanguageVersion.of(toolchainVersion()) - } + plugins.withType(JavaPlugin) { + project.sourceCompatibility=JavaVersion.VERSION_17 } - kotlin { - jvmToolchain { - languageVersion = JavaLanguageVersion.of(17) - } - } - tasks.withType(JavaCompile).configureEach { + tasks.withType(JavaCompile) { options.encoding = "UTF-8" options.compilerArgs.add("-parameters") - options.release.set(17) } } + allprojects { if (!['spring-security-bom', 'spring-security-docs'].contains(project.name)) { apply plugin: 'io.spring.javaformat' @@ -140,6 +121,12 @@ allprojects { } } } + + tasks.withType(JavaCompile).configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(17) + } + } } if (hasProperty('buildScan')) { @@ -163,12 +150,3 @@ tasks.register('cloneSamples', IncludeRepoTask) { s101 { configurationDirectory = project.file("etc/s101") } - -wrapperUpgrade { - gradle { - 'spring-security' { - repo = 'spring-projects/spring-security' - baseBranch = '6.0.x' // runs only on 6.0.x and the update is merged forward to main - } - } -} diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 0f3cea12e1..2d7927f6ba 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -9,7 +9,6 @@ sourceCompatibility = JavaVersion.VERSION_17 repositories { gradlePluginPortal() mavenCentral() - maven { url 'https://repo.spring.io/milestone' } } sourceSets { @@ -112,7 +111,6 @@ dependencies { testImplementation libs.com.squareup.okhttp3.mockwebserver } - tasks.named('test', Test).configure { onlyIf { !project.hasProperty("buildSrc.skipTests") } useJUnitPlatform() diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionPlugin.java index 48e8e805ac..9a1f95d5b4 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionPlugin.java @@ -4,6 +4,7 @@ import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.Task; import org.gradle.api.tasks.TaskProvider; import org.gradle.language.base.plugins.LifecycleBasePlugin; @@ -26,8 +27,12 @@ public class AntoraVersionPlugin implements Plugin { project.getPlugins().withType(LifecycleBasePlugin.class, new Action() { @Override public void execute(LifecycleBasePlugin lifecycleBasePlugin) { - project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME) - .configure(check -> check.dependsOn(antoraCheckVersion)); + project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME).configure(new Action() { + @Override + public void execute(Task check) { + check.dependsOn(antoraCheckVersion); + } + }); } }); project.getTasks().register("antoraUpdateVersion", UpdateAntoraVersionTask.class, new Action() { diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java index 41caa9b8d5..0791a193e8 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java @@ -35,7 +35,9 @@ public class CheckClasspathForProhibitedDependenciesPlugin implements Plugin configureProhibitedDependencyChecks(project)); + project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> { + configureProhibitedDependencyChecks(project); + }); } private void configureProhibitedDependencyChecks(Project project) { diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java index ebdde90c50..77fd369528 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java @@ -34,6 +34,8 @@ public class CheckProhibitedDependenciesLifecyclePlugin implements Plugin checkTask.dependsOn(checkProhibitedDependencies)); + project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> { + checkTask.dependsOn(checkProhibitedDependencies); + }); } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java index 907a69ef96..0eab3d8006 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java @@ -26,6 +26,7 @@ import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.DependencySet; import org.gradle.api.artifacts.repositories.ExclusiveContentRepository; +import org.gradle.api.artifacts.repositories.InclusiveRepositoryContentDescriptor; import org.gradle.api.artifacts.repositories.IvyArtifactRepository; import org.gradle.api.artifacts.repositories.IvyPatternRepositoryLayout; import org.gradle.api.tasks.JavaExec; @@ -90,7 +91,12 @@ public class GitHubChangelogPlugin implements Plugin { @Override public void execute(ExclusiveContentRepository exclusiveContentRepository) { exclusiveContentRepository.forRepositories(repository); - exclusiveContentRepository.filter(descriptor -> descriptor.includeGroup("spring-io")); + exclusiveContentRepository.filter(new Action() { + @Override + public void execute(InclusiveRepositoryContentDescriptor descriptor) { + descriptor.includeGroup("spring-io"); + } + }); } }); } diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java index 8b2f3bcd0d..792e390c00 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java @@ -114,11 +114,11 @@ public final class SpringReleaseTrainSpec { } public Builder train(int train) { - this.train = switch (train) { - case 1 -> Train.ONE; - case 2 -> Train.TWO; - default -> throw new IllegalArgumentException("Invalid train: " + train); - }; + switch (train) { + case 1: this.train = Train.ONE; break; + case 2: this.train = Train.TWO; break; + default: throw new IllegalArgumentException("Invalid train: " + train); + } return this; } @@ -156,13 +156,13 @@ public final class SpringReleaseTrainSpec { } public Builder weekOfMonth(int weekOfMonth) { - this.weekOfMonth = switch (weekOfMonth) { - case 1 -> WeekOfMonth.FIRST; - case 2 -> WeekOfMonth.SECOND; - case 3 -> WeekOfMonth.THIRD; - case 4 -> WeekOfMonth.FOURTH; - default -> throw new IllegalArgumentException("Invalid weekOfMonth: " + weekOfMonth); - }; + switch (weekOfMonth) { + case 1: this.weekOfMonth = WeekOfMonth.FIRST; break; + case 2: this.weekOfMonth = WeekOfMonth.SECOND; break; + case 3: this.weekOfMonth = WeekOfMonth.THIRD; break; + case 4: this.weekOfMonth = WeekOfMonth.FOURTH; break; + default: throw new IllegalArgumentException("Invalid weekOfMonth: " + weekOfMonth); + } return this; } @@ -172,14 +172,14 @@ public final class SpringReleaseTrainSpec { } public Builder dayOfWeek(int dayOfWeek) { - this.dayOfWeek = switch (dayOfWeek) { - case 1 -> DayOfWeek.MONDAY; - case 2 -> DayOfWeek.TUESDAY; - case 3 -> DayOfWeek.WEDNESDAY; - case 4 -> DayOfWeek.THURSDAY; - case 5 -> DayOfWeek.FRIDAY; - default -> throw new IllegalArgumentException("Invalid dayOfWeek: " + dayOfWeek); - }; + switch (dayOfWeek) { + case 1: this.dayOfWeek = DayOfWeek.MONDAY; break; + case 2: this.dayOfWeek = DayOfWeek.TUESDAY; break; + case 3: this.dayOfWeek = DayOfWeek.WEDNESDAY; break; + case 4: this.dayOfWeek = DayOfWeek.THURSDAY; break; + case 5: this.dayOfWeek = DayOfWeek.FRIDAY; break; + default: throw new IllegalArgumentException("Invalid dayOfWeek: " + dayOfWeek); + } return this; } diff --git a/buildSrc/src/main/java/org/springframework/gradle/maven/SpringSigningPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/maven/SpringSigningPlugin.java index 076872770a..112651ba2e 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/maven/SpringSigningPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/maven/SpringSigningPlugin.java @@ -21,6 +21,7 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.publish.Publication; import org.gradle.api.publish.PublishingExtension; +import org.gradle.api.publish.plugins.PublishingPlugin; import org.gradle.plugins.signing.SigningExtension; import org.gradle.plugins.signing.SigningPlugin; @@ -43,7 +44,12 @@ public class SpringSigningPlugin implements Plugin { private void sign(Project project) { SigningExtension signing = project.getExtensions().findByType(SigningExtension.class); - signing.setRequired((Callable) () -> project.getGradle().getTaskGraph().hasTask("publishArtifacts")); + signing.setRequired(new Callable() { + @Override + public Boolean call() throws Exception { + return project.getGradle().getTaskGraph().hasTask("publishArtifacts"); + } + }); String signingKeyId = (String) project.findProperty("signingKeyId"); String signingKey = (String) project.findProperty("signingKey"); String signingPassword = (String) project.findProperty("signingPassword"); diff --git a/cas/spring-security-cas.gradle b/cas/spring-security-cas.gradle deleted file mode 100644 index cebebbc9be..0000000000 --- a/cas/spring-security-cas.gradle +++ /dev/null @@ -1,25 +0,0 @@ -apply plugin: 'io.spring.convention.spring-module' - -dependencies { - management platform(project(":spring-security-dependencies")) - api project(':spring-security-core') - api project(':spring-security-web') - api 'org.apereo.cas.client:cas-client-core' - api 'org.springframework:spring-beans' - api 'org.springframework:spring-context' - api 'org.springframework:spring-core' - api 'org.springframework:spring-web' - - optional 'com.fasterxml.jackson.core:jackson-databind' - - provided 'jakarta.servlet:jakarta.servlet-api' - - testImplementation "org.assertj:assertj-core" - testImplementation "org.junit.jupiter:junit-jupiter-api" - testImplementation "org.junit.jupiter:junit-jupiter-params" - testImplementation "org.junit.jupiter:junit-jupiter-engine" - testImplementation "org.mockito:mockito-core" - testImplementation "org.mockito:mockito-junit-jupiter" - testImplementation "org.springframework:spring-test" - testImplementation 'org.skyscreamer:jsonassert' -} diff --git a/cas/src/main/java/org/springframework/security/cas/SamlServiceProperties.java b/cas/src/main/java/org/springframework/security/cas/SamlServiceProperties.java deleted file mode 100644 index 8e300f8f8e..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/SamlServiceProperties.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas; - -/** - * Sets the appropriate parameters for CAS's implementation of SAML (which is not - * guaranteed to be actually SAML compliant). - * - * @author Scott Battaglia - * @since 3.0 - */ -public final class SamlServiceProperties extends ServiceProperties { - - public static final String DEFAULT_SAML_ARTIFACT_PARAMETER = "SAMLart"; - - public static final String DEFAULT_SAML_SERVICE_PARAMETER = "TARGET"; - - public SamlServiceProperties() { - super.setArtifactParameter(DEFAULT_SAML_ARTIFACT_PARAMETER); - super.setServiceParameter(DEFAULT_SAML_SERVICE_PARAMETER); - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/ServiceProperties.java b/cas/src/main/java/org/springframework/security/cas/ServiceProperties.java deleted file mode 100644 index caf03dd62a..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/ServiceProperties.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; - -/** - * Stores properties related to this CAS service. - *

- * Each web application capable of processing CAS tickets is known as a service. This - * class stores the properties that are relevant to the local CAS service, being the - * application that is being secured by Spring Security. - * - * @author Ben Alex - */ -public class ServiceProperties implements InitializingBean { - - public static final String DEFAULT_CAS_ARTIFACT_PARAMETER = "ticket"; - - public static final String DEFAULT_CAS_SERVICE_PARAMETER = "service"; - - private String service; - - private boolean authenticateAllArtifacts; - - private boolean sendRenew = false; - - private String artifactParameter = DEFAULT_CAS_ARTIFACT_PARAMETER; - - private String serviceParameter = DEFAULT_CAS_SERVICE_PARAMETER; - - @Override - public void afterPropertiesSet() { - Assert.hasLength(this.service, "service cannot be empty."); - Assert.hasLength(this.artifactParameter, "artifactParameter cannot be empty."); - Assert.hasLength(this.serviceParameter, "serviceParameter cannot be empty."); - } - - /** - * Represents the service the user is authenticating to. - *

- * This service is the callback URL belonging to the local Spring Security System for - * Spring secured application. For example, - * - *

-	 * https://www.mycompany.com/application/login/cas
-	 * 
- * @return the URL of the service the user is authenticating to - */ - public final String getService() { - return this.service; - } - - /** - * Indicates whether the renew parameter should be sent to the CAS login - * URL and CAS validation URL. - *

- * If true, it will force CAS to authenticate the user again (even if the - * user has previously authenticated). During ticket validation it will require the - * ticket was generated as a consequence of an explicit login. High security - * applications would probably set this to true. Defaults to - * false, providing automated single sign on. - * @return whether to send the renew parameter to CAS - */ - public final boolean isSendRenew() { - return this.sendRenew; - } - - public final void setSendRenew(final boolean sendRenew) { - this.sendRenew = sendRenew; - } - - public final void setService(final String service) { - this.service = service; - } - - public final String getArtifactParameter() { - return this.artifactParameter; - } - - /** - * Configures the Request Parameter to look for when attempting to see if a CAS ticket - * was sent from the server. - * @param artifactParameter the id to use. Default is "ticket". - */ - public final void setArtifactParameter(final String artifactParameter) { - this.artifactParameter = artifactParameter; - } - - /** - * Configures the Request parameter to look for when attempting to send a request to - * CAS. - * @return the service parameter to use. Default is "service". - */ - public final String getServiceParameter() { - return this.serviceParameter; - } - - public final void setServiceParameter(final String serviceParameter) { - this.serviceParameter = serviceParameter; - } - - public final boolean isAuthenticateAllArtifacts() { - return this.authenticateAllArtifacts; - } - - /** - * If true, then any non-null artifact (ticket) should be authenticated. Additionally, - * the service will be determined dynamically in order to ensure the service matches - * the expected value for this artifact. - * @param authenticateAllArtifacts - */ - public final void setAuthenticateAllArtifacts(final boolean authenticateAllArtifacts) { - this.authenticateAllArtifacts = authenticateAllArtifacts; - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasAssertionAuthenticationToken.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasAssertionAuthenticationToken.java deleted file mode 100644 index 9d93152094..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasAssertionAuthenticationToken.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas.authentication; - -import java.util.ArrayList; - -import org.apereo.cas.client.validation.Assertion; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.SpringSecurityCoreVersion; - -/** - * Temporary authentication object needed to load the user details service. - * - * @author Scott Battaglia - * @since 3.0 - */ -public final class CasAssertionAuthenticationToken extends AbstractAuthenticationToken { - - private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; - - private final Assertion assertion; - - private final String ticket; - - public CasAssertionAuthenticationToken(final Assertion assertion, final String ticket) { - super(new ArrayList<>()); - this.assertion = assertion; - this.ticket = ticket; - } - - @Override - public Object getPrincipal() { - return this.assertion.getPrincipal().getName(); - } - - @Override - public Object getCredentials() { - return this.ticket; - } - - public Assertion getAssertion() { - return this.assertion; - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java deleted file mode 100644 index 6467a928e3..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas.authentication; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apereo.cas.client.validation.Assertion; -import org.apereo.cas.client.validation.TicketValidationException; -import org.apereo.cas.client.validation.TicketValidator; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.MessageSource; -import org.springframework.context.MessageSourceAware; -import org.springframework.context.support.MessageSourceAccessor; -import org.springframework.core.log.LogMessage; -import org.springframework.security.authentication.AccountStatusUserDetailsChecker; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.cas.ServiceProperties; -import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.SpringSecurityMessageSource; -import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; -import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; -import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper; -import org.springframework.security.core.userdetails.UserDetailsChecker; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.util.Assert; - -/** - * An {@link AuthenticationProvider} implementation that integrates with JA-SIG Central - * Authentication Service (CAS). - *

- * This AuthenticationProvider is capable of validating - * {@link CasServiceTicketAuthenticationToken} requests which contain a - * principal name equal to either - * {@link CasServiceTicketAuthenticationToken#CAS_STATEFUL_IDENTIFIER} or - * {@link CasServiceTicketAuthenticationToken#CAS_STATELESS_IDENTIFIER}. It can also - * validate a previously created {@link CasAuthenticationToken}. - * - * @author Ben Alex - * @author Scott Battaglia - */ -public class CasAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { - - private static final Log logger = LogFactory.getLog(CasAuthenticationProvider.class); - - private AuthenticationUserDetailsService authenticationUserDetailsService; - - private final UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker(); - - protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); - - private StatelessTicketCache statelessTicketCache = new NullStatelessTicketCache(); - - private String key; - - private TicketValidator ticketValidator; - - private ServiceProperties serviceProperties; - - private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); - - @Override - public void afterPropertiesSet() { - Assert.notNull(this.authenticationUserDetailsService, "An authenticationUserDetailsService must be set"); - Assert.notNull(this.ticketValidator, "A ticketValidator must be set"); - Assert.notNull(this.statelessTicketCache, "A statelessTicketCache must be set"); - Assert.hasText(this.key, - "A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated"); - Assert.notNull(this.messages, "A message source must be set"); - } - - @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { - if (!supports(authentication.getClass())) { - return null; - } - // If an existing CasAuthenticationToken, just check we created it - if (authentication instanceof CasAuthenticationToken) { - if (this.key.hashCode() != ((CasAuthenticationToken) authentication).getKeyHash()) { - throw new BadCredentialsException(this.messages.getMessage("CasAuthenticationProvider.incorrectKey", - "The presented CasAuthenticationToken does not contain the expected key")); - } - return authentication; - } - - // Ensure credentials are presented - if ((authentication.getCredentials() == null) || "".equals(authentication.getCredentials())) { - throw new BadCredentialsException(this.messages.getMessage("CasAuthenticationProvider.noServiceTicket", - "Failed to provide a CAS service ticket to validate")); - } - - boolean stateless = (authentication instanceof CasServiceTicketAuthenticationToken token - && token.isStateless()); - CasAuthenticationToken result = null; - - if (stateless) { - // Try to obtain from cache - result = this.statelessTicketCache.getByTicketId(authentication.getCredentials().toString()); - } - if (result == null) { - result = this.authenticateNow(authentication); - result.setDetails(authentication.getDetails()); - } - if (stateless) { - // Add to cache - this.statelessTicketCache.putTicketInCache(result); - } - return result; - } - - private CasAuthenticationToken authenticateNow(final Authentication authentication) throws AuthenticationException { - try { - Assertion assertion = this.ticketValidator.validate(authentication.getCredentials().toString(), - getServiceUrl(authentication)); - UserDetails userDetails = loadUserByAssertion(assertion); - this.userDetailsChecker.check(userDetails); - return new CasAuthenticationToken(this.key, userDetails, authentication.getCredentials(), - this.authoritiesMapper.mapAuthorities(userDetails.getAuthorities()), userDetails, assertion); - } - catch (TicketValidationException ex) { - throw new BadCredentialsException(ex.getMessage(), ex); - } - } - - /** - * Gets the serviceUrl. If the {@link Authentication#getDetails()} is an instance of - * {@link ServiceAuthenticationDetails}, then - * {@link ServiceAuthenticationDetails#getServiceUrl()} is used. Otherwise, the - * {@link ServiceProperties#getService()} is used. - * @param authentication - * @return - */ - private String getServiceUrl(Authentication authentication) { - String serviceUrl; - if (authentication.getDetails() instanceof ServiceAuthenticationDetails) { - return ((ServiceAuthenticationDetails) authentication.getDetails()).getServiceUrl(); - } - Assert.state(this.serviceProperties != null, - "serviceProperties cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails."); - Assert.state(this.serviceProperties.getService() != null, - "serviceProperties.getService() cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails."); - serviceUrl = this.serviceProperties.getService(); - logger.debug(LogMessage.format("serviceUrl = %s", serviceUrl)); - return serviceUrl; - } - - /** - * Template method for retrieving the UserDetails based on the assertion. Default is - * to call configured userDetailsService and pass the username. Deployers can override - * this method and retrieve the user based on any criteria they desire. - * @param assertion The CAS Assertion. - * @return the UserDetails. - */ - protected UserDetails loadUserByAssertion(final Assertion assertion) { - final CasAssertionAuthenticationToken token = new CasAssertionAuthenticationToken(assertion, ""); - return this.authenticationUserDetailsService.loadUserDetails(token); - } - - @SuppressWarnings("unchecked") - /** - * Sets the UserDetailsService to use. This is a convenience method to invoke - */ - public void setUserDetailsService(final UserDetailsService userDetailsService) { - this.authenticationUserDetailsService = new UserDetailsByNameServiceWrapper(userDetailsService); - } - - public void setAuthenticationUserDetailsService( - final AuthenticationUserDetailsService authenticationUserDetailsService) { - this.authenticationUserDetailsService = authenticationUserDetailsService; - } - - public void setServiceProperties(final ServiceProperties serviceProperties) { - this.serviceProperties = serviceProperties; - } - - protected String getKey() { - return this.key; - } - - public void setKey(String key) { - this.key = key; - } - - public StatelessTicketCache getStatelessTicketCache() { - return this.statelessTicketCache; - } - - protected TicketValidator getTicketValidator() { - return this.ticketValidator; - } - - @Override - public void setMessageSource(final MessageSource messageSource) { - this.messages = new MessageSourceAccessor(messageSource); - } - - public void setStatelessTicketCache(final StatelessTicketCache statelessTicketCache) { - this.statelessTicketCache = statelessTicketCache; - } - - public void setTicketValidator(final TicketValidator ticketValidator) { - this.ticketValidator = ticketValidator; - } - - public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { - this.authoritiesMapper = authoritiesMapper; - } - - @Override - public boolean supports(final Class authentication) { - return (CasServiceTicketAuthenticationToken.class.isAssignableFrom(authentication)) - || (CasAuthenticationToken.class.isAssignableFrom(authentication)) - || (CasAssertionAuthenticationToken.class.isAssignableFrom(authentication)); - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java deleted file mode 100644 index 7efdf8e939..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas.authentication; - -import java.io.Serializable; -import java.util.Collection; - -import org.apereo.cas.client.validation.Assertion; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.SpringSecurityCoreVersion; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; - -/** - * Represents a successful CAS Authentication. - * - * @author Ben Alex - * @author Scott Battaglia - */ -public class CasAuthenticationToken extends AbstractAuthenticationToken implements Serializable { - - private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; - - private final Object credentials; - - private final Object principal; - - private final UserDetails userDetails; - - private final int keyHash; - - private final Assertion assertion; - - /** - * Constructor. - * @param key to identify if this object made by a given - * {@link CasAuthenticationProvider} - * @param principal typically the UserDetails object (cannot be null) - * @param credentials the service/proxy ticket ID from CAS (cannot be - * null) - * @param authorities the authorities granted to the user (from the - * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot - * be null) - * @param userDetails the user details (from the - * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot - * be null) - * @param assertion the assertion returned from the CAS servers. It contains the - * principal and how to obtain a proxy ticket for the user. - * @throws IllegalArgumentException if a null was passed - */ - public CasAuthenticationToken(final String key, final Object principal, final Object credentials, - final Collection authorities, final UserDetails userDetails, - final Assertion assertion) { - this(extractKeyHash(key), principal, credentials, authorities, userDetails, assertion); - } - - /** - * Private constructor for Jackson Deserialization support - * @param keyHash hashCode of provided key to identify if this object made by a given - * {@link CasAuthenticationProvider} - * @param principal typically the UserDetails object (cannot be null) - * @param credentials the service/proxy ticket ID from CAS (cannot be - * null) - * @param authorities the authorities granted to the user (from the - * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot - * be null) - * @param userDetails the user details (from the - * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot - * be null) - * @param assertion the assertion returned from the CAS servers. It contains the - * principal and how to obtain a proxy ticket for the user. - * @throws IllegalArgumentException if a null was passed - * @since 4.2 - */ - private CasAuthenticationToken(final Integer keyHash, final Object principal, final Object credentials, - final Collection authorities, final UserDetails userDetails, - final Assertion assertion) { - super(authorities); - if ((principal == null) || "".equals(principal) || (credentials == null) || "".equals(credentials) - || (authorities == null) || (userDetails == null) || (assertion == null)) { - throw new IllegalArgumentException("Cannot pass null or empty values to constructor"); - } - this.keyHash = keyHash; - this.principal = principal; - this.credentials = credentials; - this.userDetails = userDetails; - this.assertion = assertion; - setAuthenticated(true); - } - - private static Integer extractKeyHash(String key) { - Assert.hasLength(key, "key cannot be null or empty"); - return key.hashCode(); - } - - @Override - public boolean equals(final Object obj) { - if (!super.equals(obj)) { - return false; - } - if (obj instanceof CasAuthenticationToken) { - CasAuthenticationToken test = (CasAuthenticationToken) obj; - if (!this.assertion.equals(test.getAssertion())) { - return false; - } - if (this.getKeyHash() != test.getKeyHash()) { - return false; - } - return true; - } - return false; - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + this.credentials.hashCode(); - result = 31 * result + this.principal.hashCode(); - result = 31 * result + this.userDetails.hashCode(); - result = 31 * result + this.keyHash; - result = 31 * result + ObjectUtils.nullSafeHashCode(this.assertion); - return result; - } - - @Override - public Object getCredentials() { - return this.credentials; - } - - public int getKeyHash() { - return this.keyHash; - } - - @Override - public Object getPrincipal() { - return this.principal; - } - - public Assertion getAssertion() { - return this.assertion; - } - - public UserDetails getUserDetails() { - return this.userDetails; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(super.toString()); - sb.append(" Assertion: ").append(this.assertion); - sb.append(" Credentials (Service/Proxy Ticket): ").append(this.credentials); - return (sb.toString()); - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java deleted file mode 100644 index d4471b6c57..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2002-2023 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.cas.authentication; - -import java.io.Serial; -import java.util.Collection; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.SpringSecurityCoreVersion; -import org.springframework.util.Assert; - -/** - * An {@link org.springframework.security.core.Authentication} implementation that is - * designed to process CAS service ticket. - * - * @author Hal Deadman - * @since 6.1 - */ -public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationToken { - - static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_"; - - static final String CAS_STATEFUL_IDENTIFIER = "_cas_stateful_"; - - @Serial - private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; - - private final String identifier; - - private Object credentials; - - /** - * This constructor can be safely used by any code that wishes to create a - * CasServiceTicketAuthenticationToken, as the {@link #isAuthenticated()} - * will return false. - * - */ - public CasServiceTicketAuthenticationToken(String identifier, Object credentials) { - super(null); - this.identifier = identifier; - this.credentials = credentials; - setAuthenticated(false); - } - - /** - * This constructor should only be used by AuthenticationManager or - * AuthenticationProvider implementations that are satisfied with - * producing a trusted (i.e. {@link #isAuthenticated()} = true) - * authentication token. - * @param identifier - * @param credentials - * @param authorities - */ - public CasServiceTicketAuthenticationToken(String identifier, Object credentials, - Collection authorities) { - super(authorities); - this.identifier = identifier; - this.credentials = credentials; - super.setAuthenticated(true); - } - - public static CasServiceTicketAuthenticationToken stateful(Object credentials) { - return new CasServiceTicketAuthenticationToken(CAS_STATEFUL_IDENTIFIER, credentials); - } - - public static CasServiceTicketAuthenticationToken stateless(Object credentials) { - return new CasServiceTicketAuthenticationToken(CAS_STATELESS_IDENTIFIER, credentials); - } - - public boolean isStateless() { - return CAS_STATELESS_IDENTIFIER.equals(this.identifier); - } - - @Override - public Object getCredentials() { - return this.credentials; - } - - @Override - public Object getPrincipal() { - return this.identifier; - } - - @Override - public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { - Assert.isTrue(!isAuthenticated, - "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); - super.setAuthenticated(false); - } - - @Override - public void eraseCredentials() { - super.eraseCredentials(); - this.credentials = null; - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/NullStatelessTicketCache.java b/cas/src/main/java/org/springframework/security/cas/authentication/NullStatelessTicketCache.java deleted file mode 100644 index 4284161a39..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/authentication/NullStatelessTicketCache.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas.authentication; - -/** - * Implementation of @link {@link StatelessTicketCache} that has no backing cache. Useful - * in instances where storing of tickets for stateless session management is not required. - *

- * This is the default StatelessTicketCache of the @link {@link CasAuthenticationProvider} - * to eliminate the unnecessary dependency on EhCache that applications have even if they - * are not using the stateless session management. - * - * @author Scott Battaglia - * @see CasAuthenticationProvider - */ -public final class NullStatelessTicketCache implements StatelessTicketCache { - - /** - * @return null since we are not storing any tickets. - */ - @Override - public CasAuthenticationToken getByTicketId(final String serviceTicket) { - return null; - } - - /** - * This is a no-op since we are not storing tickets. - */ - @Override - public void putTicketInCache(final CasAuthenticationToken token) { - // nothing to do - } - - /** - * This is a no-op since we are not storing tickets. - */ - @Override - public void removeTicketFromCache(final CasAuthenticationToken token) { - // nothing to do - } - - /** - * This is a no-op since we are not storing tickets. - */ - @Override - public void removeTicketFromCache(final String serviceTicket) { - // nothing to do - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCache.java b/cas/src/main/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCache.java deleted file mode 100644 index a07148cd5f..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCache.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2002-2023 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.cas.authentication; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.cache.Cache; -import org.springframework.core.log.LogMessage; -import org.springframework.util.Assert; - -/** - * Caches tickets using a Spring IoC defined {@link Cache}. - * - * @author Marten Deinum - * @since 3.2 - * - */ -public class SpringCacheBasedTicketCache implements StatelessTicketCache { - - private static final Log logger = LogFactory.getLog(SpringCacheBasedTicketCache.class); - - private final Cache cache; - - public SpringCacheBasedTicketCache(Cache cache) { - Assert.notNull(cache, "cache mandatory"); - this.cache = cache; - } - - @Override - public CasAuthenticationToken getByTicketId(final String serviceTicket) { - final Cache.ValueWrapper element = (serviceTicket != null) ? this.cache.get(serviceTicket) : null; - logger.debug(LogMessage.of(() -> "Cache hit: " + (element != null) + "; service ticket: " + serviceTicket)); - return (element != null) ? (CasAuthenticationToken) element.get() : null; - } - - @Override - public void putTicketInCache(final CasAuthenticationToken token) { - String key = token.getCredentials().toString(); - logger.debug(LogMessage.of(() -> "Cache put: " + key)); - this.cache.put(key, token); - } - - @Override - public void removeTicketFromCache(final CasAuthenticationToken token) { - logger.debug(LogMessage.of(() -> "Cache remove: " + token.getCredentials().toString())); - this.removeTicketFromCache(token.getCredentials().toString()); - } - - @Override - public void removeTicketFromCache(final String serviceTicket) { - this.cache.evict(serviceTicket); - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/StatelessTicketCache.java b/cas/src/main/java/org/springframework/security/cas/authentication/StatelessTicketCache.java deleted file mode 100644 index 74df6bb9df..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/authentication/StatelessTicketCache.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2004 Acegi Technology Pty Limited - * - * 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.cas.authentication; - -/** - * Caches CAS service tickets and CAS proxy tickets for stateless connections. - * - *

- * When a service ticket or proxy ticket is validated against the CAS server, it is unable - * to be used again. Most types of callers are stateful and are associated with a given - * HttpSession. This allows the affirmative CAS validation outcome to be - * stored in the HttpSession, meaning the removal of the ticket from the CAS - * server is not an issue. - *

- * - *

- * Stateless callers, such as remoting protocols, cannot take advantage of - * HttpSession. If the stateless caller is located a significant network - * distance from the CAS server, acquiring a fresh service ticket or proxy ticket for each - * invocation would be expensive. - *

- * - *

- * To avoid this issue with stateless callers, it is expected stateless callers will - * obtain a single service ticket or proxy ticket, and then present this same ticket to - * the Spring Security secured application on each occasion. As no - * HttpSession is available for such callers, the affirmative CAS validation - * outcome cannot be stored in this location. - *

- * - *

- * The StatelessTicketCache enables the service tickets and proxy tickets - * belonging to stateless callers to be placed in a cache. This in-memory cache stores the - * CasAuthenticationToken, effectively providing the same capability as a - * HttpSession with the ticket identifier being the key rather than a session - * identifier. - *

- * - *

- * Implementations should provide a reasonable timeout on stored entries, such that the - * stateless caller are not required to unnecessarily acquire fresh CAS service tickets or - * proxy tickets. - *

- * - * @author Ben Alex - */ -public interface StatelessTicketCache { - - /** - * Retrieves the CasAuthenticationToken associated with the specified - * ticket. - * - *

- * If not found, returns a nullCasAuthenticationToken. - *

- * @return the fully populated authentication token - */ - CasAuthenticationToken getByTicketId(String serviceTicket); - - /** - * Adds the specified CasAuthenticationToken to the cache. - * - *

- * The {@link CasAuthenticationToken#getCredentials()} method is used to retrieve the - * service ticket number. - *

- * @param token to be added to the cache - */ - void putTicketInCache(CasAuthenticationToken token); - - /** - * Removes the specified ticket from the cache, as per - * {@link #removeTicketFromCache(String)}. - * - *

- * Implementations should use {@link CasAuthenticationToken#getCredentials()} to - * obtain the ticket and then delegate to the {@link #removeTicketFromCache(String)} - * method. - *

- * @param token to be removed - */ - void removeTicketFromCache(CasAuthenticationToken token); - - /** - * Removes the specified ticket from the cache, meaning that future calls will require - * a new service ticket. - * - *

- * This is in case applications wish to provide a session termination capability for - * their stateless clients. - *

- * @param serviceTicket to be removed - */ - void removeTicketFromCache(String serviceTicket); - -} diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/package-info.java b/cas/src/main/java/org/springframework/security/cas/authentication/package-info.java deleted file mode 100644 index e3c7518dbf..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/authentication/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2002-2023 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. - */ - -/** - * An {@code AuthenticationProvider} that can process CAS service tickets and proxy - * tickets. - */ -package org.springframework.security.cas.authentication; diff --git a/cas/src/main/java/org/springframework/security/cas/jackson2/AssertionImplMixin.java b/cas/src/main/java/org/springframework/security/cas/jackson2/AssertionImplMixin.java deleted file mode 100644 index e5e48617b7..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/jackson2/AssertionImplMixin.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2015-2023 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.cas.jackson2; - -import java.util.Date; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.apereo.cas.client.authentication.AttributePrincipal; - -/** - * Helps in jackson deserialization of class - * {@link org.apereo.cas.client.validation.AssertionImpl}, which is used with - * {@link org.springframework.security.cas.authentication.CasAuthenticationToken}. To use - * this class we need to register with - * {@link com.fasterxml.jackson.databind.ObjectMapper}. Type information will be stored - * in @class property. - *

- *

- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new CasJackson2Module());
- * 
- * - * @author Jitendra Singh - * @since 4.2 - * @see CasJackson2Module - * @see org.springframework.security.jackson2.SecurityJackson2Modules - */ -@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) -@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, - isGetterVisibility = JsonAutoDetect.Visibility.NONE) -@JsonIgnoreProperties(ignoreUnknown = true) -class AssertionImplMixin { - - /** - * Mixin Constructor helps in deserialize - * {@link org.apereo.cas.client.validation.AssertionImpl} - * @param principal the Principal to associate with the Assertion. - * @param validFromDate when the assertion is valid from. - * @param validUntilDate when the assertion is valid to. - * @param authenticationDate when the assertion is authenticated. - * @param attributes the key/value pairs for this attribute. - */ - @JsonCreator - AssertionImplMixin(@JsonProperty("principal") AttributePrincipal principal, - @JsonProperty("validFromDate") Date validFromDate, @JsonProperty("validUntilDate") Date validUntilDate, - @JsonProperty("authenticationDate") Date authenticationDate, - @JsonProperty("attributes") Map attributes) { - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/jackson2/AttributePrincipalImplMixin.java b/cas/src/main/java/org/springframework/security/cas/jackson2/AttributePrincipalImplMixin.java deleted file mode 100644 index ef9e3e8654..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/jackson2/AttributePrincipalImplMixin.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2015-2023 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.cas.jackson2; - -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.apereo.cas.client.proxy.ProxyRetriever; - -/** - * Helps in deserialize - * {@link org.apereo.cas.client.authentication.AttributePrincipalImpl} which is used with - * {@link org.springframework.security.cas.authentication.CasAuthenticationToken}. Type - * information will be stored in property named @class. - *

- *

- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new CasJackson2Module());
- * 
- * - * @author Jitendra Singh - * @since 4.2 - * @see CasJackson2Module - * @see org.springframework.security.jackson2.SecurityJackson2Modules - */ -@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) -@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, - isGetterVisibility = JsonAutoDetect.Visibility.NONE) -@JsonIgnoreProperties(ignoreUnknown = true) -class AttributePrincipalImplMixin { - - /** - * Mixin Constructor helps in deserialize - * {@link org.apereo.cas.client.authentication.AttributePrincipalImpl} - * @param name the unique identifier for the principal. - * @param attributes the key/value pairs for this principal. - * @param proxyGrantingTicket the ticket associated with this principal. - * @param proxyRetriever the ProxyRetriever implementation to call back to the CAS - * server. - */ - @JsonCreator - AttributePrincipalImplMixin(@JsonProperty("name") String name, - @JsonProperty("attributes") Map attributes, - @JsonProperty("proxyGrantingTicket") String proxyGrantingTicket, - @JsonProperty("proxyRetriever") ProxyRetriever proxyRetriever) { - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixin.java b/cas/src/main/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixin.java deleted file mode 100644 index 26aa5effd3..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixin.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2015-2023 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.cas.jackson2; - -import java.util.Collection; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.apereo.cas.client.validation.Assertion; - -import org.springframework.security.cas.authentication.CasAuthenticationProvider; -import org.springframework.security.cas.authentication.CasAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -/** - * Mixin class which helps in deserialize - * {@link org.springframework.security.cas.authentication.CasAuthenticationToken} using - * jackson. Two more dependent classes needs to register along with this mixin class. - *
    - *
  1. {@link org.springframework.security.cas.jackson2.AssertionImplMixin}
  2. - *
  3. {@link org.springframework.security.cas.jackson2.AttributePrincipalImplMixin}
  4. - *
- * - *

- * - *

- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new CasJackson2Module());
- * 
- * - * @author Jitendra Singh - * @since 4.2 - * @see CasJackson2Module - * @see org.springframework.security.jackson2.SecurityJackson2Modules - */ -@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) -@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, isGetterVisibility = JsonAutoDetect.Visibility.NONE, - getterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.ANY) -@JsonIgnoreProperties(ignoreUnknown = true) -class CasAuthenticationTokenMixin { - - /** - * Mixin Constructor helps in deserialize {@link CasAuthenticationToken} - * @param keyHash hashCode of provided key to identify if this object made by a given - * {@link CasAuthenticationProvider} - * @param principal typically the UserDetails object (cannot be null) - * @param credentials the service/proxy ticket ID from CAS (cannot be - * null) - * @param authorities the authorities granted to the user (from the - * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot - * be null) - * @param userDetails the user details (from the - * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot - * be null) - * @param assertion the assertion returned from the CAS servers. It contains the - * principal and how to obtain a proxy ticket for the user. - */ - @JsonCreator - CasAuthenticationTokenMixin(@JsonProperty("keyHash") Integer keyHash, @JsonProperty("principal") Object principal, - @JsonProperty("credentials") Object credentials, - @JsonProperty("authorities") Collection authorities, - @JsonProperty("userDetails") UserDetails userDetails, @JsonProperty("assertion") Assertion assertion) { - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/jackson2/CasJackson2Module.java b/cas/src/main/java/org/springframework/security/cas/jackson2/CasJackson2Module.java deleted file mode 100644 index b6c7c6f8fa..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/jackson2/CasJackson2Module.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2015-2023 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.cas.jackson2; - -import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.databind.module.SimpleModule; -import org.apereo.cas.client.authentication.AttributePrincipalImpl; -import org.apereo.cas.client.validation.AssertionImpl; - -import org.springframework.security.cas.authentication.CasAuthenticationToken; -import org.springframework.security.jackson2.SecurityJackson2Modules; - -/** - * Jackson module for spring-security-cas. This module register - * {@link AssertionImplMixin}, {@link AttributePrincipalImplMixin} and - * {@link CasAuthenticationTokenMixin}. If no default typing enabled by default then it'll - * enable it because typing info is needed to properly serialize/deserialize objects. In - * order to use this module just add this module into your ObjectMapper configuration. - * - *
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new CasJackson2Module());
- * 
Note: use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get list - * of all security modules on the classpath. - * - * @author Jitendra Singh. - * @since 4.2 - * @see org.springframework.security.jackson2.SecurityJackson2Modules - */ -public class CasJackson2Module extends SimpleModule { - - public CasJackson2Module() { - super(CasJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null)); - } - - @Override - public void setupModule(SetupContext context) { - SecurityJackson2Modules.enableDefaultTyping(context.getOwner()); - context.setMixInAnnotations(AssertionImpl.class, AssertionImplMixin.class); - context.setMixInAnnotations(AttributePrincipalImpl.class, AttributePrincipalImplMixin.class); - context.setMixInAnnotations(CasAuthenticationToken.class, CasAuthenticationTokenMixin.class); - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/package-info.java b/cas/src/main/java/org/springframework/security/cas/package-info.java deleted file mode 100644 index d11703d82c..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2002-2023 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. - */ - -/** - * Spring Security support for Apereo's Central Authentication Service - * (CAS). - */ -package org.springframework.security.cas; diff --git a/cas/src/main/java/org/springframework/security/cas/userdetails/AbstractCasAssertionUserDetailsService.java b/cas/src/main/java/org/springframework/security/cas/userdetails/AbstractCasAssertionUserDetailsService.java deleted file mode 100644 index 2a1ccfc9ef..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/userdetails/AbstractCasAssertionUserDetailsService.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas.userdetails; - -import org.apereo.cas.client.validation.Assertion; - -import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken; -import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; -import org.springframework.security.core.userdetails.UserDetails; - -/** - * Abstract class for using the provided CAS assertion to construct a new User object. - * This generally is most useful when combined with a SAML-based response from the CAS - * Server/client. - * - * @author Scott Battaglia - * @since 3.0 - */ -public abstract class AbstractCasAssertionUserDetailsService - implements AuthenticationUserDetailsService { - - @Override - public final UserDetails loadUserDetails(final CasAssertionAuthenticationToken token) { - return loadUserDetails(token.getAssertion()); - } - - /** - * Protected template method for construct a - * {@link org.springframework.security.core.userdetails.UserDetails} via the supplied - * CAS assertion. - * @param assertion the assertion to use to construct the new UserDetails. CANNOT be - * NULL. - * @return the newly constructed UserDetails. - */ - protected abstract UserDetails loadUserDetails(Assertion assertion); - -} diff --git a/cas/src/main/java/org/springframework/security/cas/userdetails/GrantedAuthorityFromAssertionAttributesUserDetailsService.java b/cas/src/main/java/org/springframework/security/cas/userdetails/GrantedAuthorityFromAssertionAttributesUserDetailsService.java deleted file mode 100644 index df19e1033a..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/userdetails/GrantedAuthorityFromAssertionAttributesUserDetailsService.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas.userdetails; - -import java.util.ArrayList; -import java.util.List; - -import org.apereo.cas.client.validation.Assertion; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.util.Assert; - -/** - * Populates the {@link org.springframework.security.core.GrantedAuthority}s for a user by - * reading a list of attributes that were returned as part of the CAS response. Each - * attribute is read and each value of the attribute is turned into a GrantedAuthority. If - * the attribute has no value then its not added. - * - * @author Scott Battaglia - * @since 3.0 - */ -public final class GrantedAuthorityFromAssertionAttributesUserDetailsService - extends AbstractCasAssertionUserDetailsService { - - private static final String NON_EXISTENT_PASSWORD_VALUE = "NO_PASSWORD"; - - private final String[] attributes; - - private boolean convertToUpperCase = true; - - public GrantedAuthorityFromAssertionAttributesUserDetailsService(final String[] attributes) { - Assert.notNull(attributes, "attributes cannot be null."); - Assert.isTrue(attributes.length > 0, "At least one attribute is required to retrieve roles from."); - this.attributes = attributes; - } - - @SuppressWarnings("unchecked") - @Override - protected UserDetails loadUserDetails(final Assertion assertion) { - List grantedAuthorities = new ArrayList<>(); - for (String attribute : this.attributes) { - Object value = assertion.getPrincipal().getAttributes().get(attribute); - if (value != null) { - if (value instanceof List) { - for (Object o : (List) value) { - grantedAuthorities.add(createSimpleGrantedAuthority(o)); - } - } - else { - grantedAuthorities.add(createSimpleGrantedAuthority(value)); - } - } - } - return new User(assertion.getPrincipal().getName(), NON_EXISTENT_PASSWORD_VALUE, true, true, true, true, - grantedAuthorities); - } - - private SimpleGrantedAuthority createSimpleGrantedAuthority(Object o) { - return new SimpleGrantedAuthority(this.convertToUpperCase ? o.toString().toUpperCase() : o.toString()); - } - - /** - * Converts the returned attribute values to uppercase values. - * @param convertToUpperCase true if it should convert, false otherwise. - */ - public void setConvertToUpperCase(final boolean convertToUpperCase) { - this.convertToUpperCase = convertToUpperCase; - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java b/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java deleted file mode 100644 index 3f8661614a..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2002-2023 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.cas.web; - -import java.io.IOException; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.apereo.cas.client.util.CommonUtils; -import org.apereo.cas.client.util.WebUtils; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.security.cas.ServiceProperties; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.DefaultRedirectStrategy; -import org.springframework.util.Assert; - -/** - * Used by the ExceptionTranslationFilter to commence authentication via the - * JA-SIG Central Authentication Service (CAS). - *

- * The user's browser will be redirected to the JA-SIG CAS enterprise-wide login page. - * This page is specified by the loginUrl property. Once login is complete, - * the CAS login page will redirect to the page indicated by the service - * property. The service is a HTTP URL belonging to the current application. - * The service URL is monitored by the {@link CasAuthenticationFilter}, which - * will validate the CAS login was successful. - * - * @author Ben Alex - * @author Scott Battaglia - */ -public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean { - - private ServiceProperties serviceProperties; - - private String loginUrl; - - /** - * Determines whether the Service URL should include the session id for the specific - * user. As of CAS 3.0.5, the session id will automatically be stripped. However, - * older versions of CAS (i.e. CAS 2), do not automatically strip the session - * identifier (this is a bug on the part of the older server implementations), so an - * option to disable the session encoding is provided for backwards compatibility. - * - * By default, encoding is enabled. - */ - private boolean encodeServiceUrlWithSessionId = true; - - @Override - public void afterPropertiesSet() { - Assert.hasLength(this.loginUrl, "loginUrl must be specified"); - Assert.notNull(this.serviceProperties, "serviceProperties must be specified"); - Assert.notNull(this.serviceProperties.getService(), "serviceProperties.getService() cannot be null."); - } - - @Override - public final void commence(final HttpServletRequest servletRequest, HttpServletResponse response, - AuthenticationException authenticationException) throws IOException { - String urlEncodedService = createServiceUrl(servletRequest, response); - String redirectUrl = createRedirectUrl(urlEncodedService); - preCommence(servletRequest, response); - new DefaultRedirectStrategy().sendRedirect(servletRequest, response, redirectUrl); - // response.sendRedirect(redirectUrl); - } - - /** - * Constructs a new Service Url. The default implementation relies on the CAS client - * to do the bulk of the work. - * @param request the HttpServletRequest - * @param response the HttpServlet Response - * @return the constructed service url. CANNOT be NULL. - */ - protected String createServiceUrl(HttpServletRequest request, HttpServletResponse response) { - return WebUtils.constructServiceUrl(null, response, this.serviceProperties.getService(), null, - this.serviceProperties.getArtifactParameter(), this.encodeServiceUrlWithSessionId); - } - - /** - * Constructs the Url for Redirection to the CAS server. Default implementation relies - * on the CAS client to do the bulk of the work. - * @param serviceUrl the service url that should be included. - * @return the redirect url. CANNOT be NULL. - */ - protected String createRedirectUrl(String serviceUrl) { - return CommonUtils.constructRedirectUrl(this.loginUrl, this.serviceProperties.getServiceParameter(), serviceUrl, - this.serviceProperties.isSendRenew(), false); - } - - /** - * Template method for you to do your own pre-processing before the redirect occurs. - * @param request the HttpServletRequest - * @param response the HttpServletResponse - */ - protected void preCommence(HttpServletRequest request, HttpServletResponse response) { - - } - - /** - * The enterprise-wide CAS login URL. Usually something like - * https://www.mycompany.com/cas/login. - * @return the enterprise-wide CAS login URL - */ - public final String getLoginUrl() { - return this.loginUrl; - } - - public final ServiceProperties getServiceProperties() { - return this.serviceProperties; - } - - public final void setLoginUrl(String loginUrl) { - this.loginUrl = loginUrl; - } - - public final void setServiceProperties(ServiceProperties serviceProperties) { - this.serviceProperties = serviceProperties; - } - - /** - * Sets whether to encode the service url with the session id or not. - * @param encodeServiceUrlWithSessionId whether to encode the service url with the - * session id or not. - */ - public final void setEncodeServiceUrlWithSessionId(boolean encodeServiceUrlWithSessionId) { - this.encodeServiceUrlWithSessionId = encodeServiceUrlWithSessionId; - } - - /** - * Sets whether to encode the service url with the session id or not. - * @return whether to encode the service url with the session id or not. - * - */ - protected boolean getEncodeServiceUrlWithSessionId() { - return this.encodeServiceUrlWithSessionId; - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java b/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java deleted file mode 100644 index 94c783e3a2..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Copyright 2002-2023 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.cas.web; - -import java.io.IOException; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage; -import org.apereo.cas.client.util.WebUtils; -import org.apereo.cas.client.validation.TicketValidator; - -import org.springframework.core.log.LogMessage; -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.authentication.AuthenticationDetailsSource; -import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent; -import org.springframework.security.cas.ServiceProperties; -import org.springframework.security.cas.authentication.CasServiceTicketAuthenticationToken; -import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails; -import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.context.SecurityContextHolderStrategy; -import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; -import org.springframework.security.web.context.HttpSessionSecurityContextRepository; -import org.springframework.security.web.context.SecurityContextRepository; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.Assert; - -/** - * Processes a CAS service ticket, obtains proxy granting tickets, and processes proxy - * tickets. - *

Service Tickets

- *

- * A service ticket consists of an opaque ticket string. It arrives at this filter by the - * user's browser successfully authenticating using CAS, and then receiving a HTTP - * redirect to a service. The opaque ticket string is presented in the - * ticket request parameter. - *

- * This filter monitors the service URL so it can receive the service ticket - * and process it. By default this filter processes the URL /login/cas. When - * processing this URL, the value of {@link ServiceProperties#getService()} is used as the - * service when validating the ticket. This means that it is - * important that {@link ServiceProperties#getService()} specifies the same value as the - * filterProcessesUrl. - *

- * Processing the service ticket involves creating a - * CasServiceTicketAuthenticationToken which uses - * {@link CasServiceTicketAuthenticationToken#CAS_STATEFUL_IDENTIFIER} for the - * principal and the opaque ticket string as the credentials. - *

Obtaining Proxy Granting Tickets

- *

- * If specified, the filter can also monitor the proxyReceptorUrl. The filter - * will respond to requests matching this url so that the CAS Server can provide a PGT to - * the filter. Note that in addition to the proxyReceptorUrl a non-null - * proxyGrantingTicketStorage must be provided in order for the filter to - * respond to proxy receptor requests. By configuring a shared - * {@link ProxyGrantingTicketStorage} between the {@link TicketValidator} and the - * CasAuthenticationFilter one can have the CasAuthenticationFilter handle the proxying - * requirements for CAS. - *

Proxy Tickets

- *

- * The filter can process tickets present on any url. This is useful when wanting to - * process proxy tickets. In order for proxy tickets to get processed - * {@link ServiceProperties#isAuthenticateAllArtifacts()} must return true. - * Additionally, if the request is already authenticated, authentication will not - * occur. Last, {@link AuthenticationDetailsSource#buildDetails(Object)} must return a - * {@link ServiceAuthenticationDetails}. This can be accomplished using the - * {@link ServiceAuthenticationDetailsSource}. In this case - * {@link ServiceAuthenticationDetails#getServiceUrl()} will be used for the service url. - *

- * Processing the proxy ticket involves creating a - * CasServiceTicketAuthenticationToken which uses - * {@link CasServiceTicketAuthenticationToken#CAS_STATELESS_IDENTIFIER} for the - * principal and the opaque ticket string as the credentials. - * When a proxy ticket is successfully authenticated, the FilterChain continues and the - * authenticationSuccessHandler is not used. - *

Notes about the AuthenticationManager

- *

- * The configured AuthenticationManager is expected to provide a provider - * that can recognise CasServiceTicketAuthenticationTokens containing this - * special principal name, and process them accordingly by validation with - * the CAS server. Additionally, it should be capable of using the result of - * {@link ServiceAuthenticationDetails#getServiceUrl()} as the service when validating the - * ticket. - *

Example Configuration

- *

- * An example configuration that supports service tickets, obtaining proxy granting - * tickets, and proxy tickets is illustrated below: - * - *

- * <b:bean id="serviceProperties"
- *     class="org.springframework.security.cas.ServiceProperties"
- *     p:service="https://service.example.com/cas-sample/login/cas"
- *     p:authenticateAllArtifacts="true"/>
- * <b:bean id="casEntryPoint"
- *     class="org.springframework.security.cas.web.CasAuthenticationEntryPoint"
- *     p:serviceProperties-ref="serviceProperties" p:loginUrl="https://login.example.org/cas/login" />
- * <b:bean id="casFilter"
- *     class="org.springframework.security.cas.web.CasAuthenticationFilter"
- *     p:authenticationManager-ref="authManager"
- *     p:serviceProperties-ref="serviceProperties"
- *     p:proxyGrantingTicketStorage-ref="pgtStorage"
- *     p:proxyReceptorUrl="/login/cas/proxyreceptor">
- *     <b:property name="authenticationDetailsSource">
- *         <b:bean class="org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource"/>
- *     </b:property>
- *     <b:property name="authenticationFailureHandler">
- *         <b:bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"
- *             p:defaultFailureUrl="/casfailed.jsp"/>
- *     </b:property>
- * </b:bean>
- * <!--
- *     NOTE: In a real application you should not use an in memory implementation. You will also want
- *           to ensure to clean up expired tickets by calling ProxyGrantingTicketStorage.cleanup()
- *  -->
- * <b:bean id="pgtStorage" class="org.apereo.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>
- * <b:bean id="casAuthProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider"
- *     p:serviceProperties-ref="serviceProperties"
- *     p:key="casAuthProviderKey">
- *     <b:property name="authenticationUserDetailsService">
- *         <b:bean
- *             class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
- *             <b:constructor-arg ref="userService" />
- *         </b:bean>
- *     </b:property>
- *     <b:property name="ticketValidator">
- *         <b:bean
- *             class="org.apereo.cas.client.validation.Cas20ProxyTicketValidator"
- *             p:acceptAnyProxy="true"
- *             p:proxyCallbackUrl="https://service.example.com/cas-sample/login/cas/proxyreceptor"
- *             p:proxyGrantingTicketStorage-ref="pgtStorage">
- *             <b:constructor-arg value="https://login.example.org/cas" />
- *         </b:bean>
- *     </b:property>
- *     <b:property name="statelessTicketCache">
- *         <b:bean class="org.springframework.security.cas.authentication.EhCacheBasedTicketCache">
- *             <b:property name="cache">
- *                 <b:bean class="net.sf.ehcache.Cache"
- *                   init-method="initialise"
- *                   destroy-method="dispose">
- *                     <b:constructor-arg value="casTickets"/>
- *                     <b:constructor-arg value="50"/>
- *                     <b:constructor-arg value="true"/>
- *                     <b:constructor-arg value="false"/>
- *                     <b:constructor-arg value="3600"/>
- *                     <b:constructor-arg value="900"/>
- *                 </b:bean>
- *             </b:property>
- *         </b:bean>
- *     </b:property>
- * </b:bean>
- * 
- * - * @author Ben Alex - * @author Rob Winch - */ -public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFilter { - - /** - * The last portion of the receptor url, i.e. /proxy/receptor - */ - private RequestMatcher proxyReceptorMatcher; - - /** - * The backing storage to store ProxyGrantingTicket requests. - */ - private ProxyGrantingTicketStorage proxyGrantingTicketStorage; - - private String artifactParameter = ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER; - - private boolean authenticateAllArtifacts; - - private AuthenticationFailureHandler proxyFailureHandler = new SimpleUrlAuthenticationFailureHandler(); - - private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); - - private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder - .getContextHolderStrategy(); - - public CasAuthenticationFilter() { - super("/login/cas"); - setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler()); - setSecurityContextRepository(this.securityContextRepository); - } - - @Override - protected final void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, - FilterChain chain, Authentication authResult) throws IOException, ServletException { - boolean continueFilterChain = proxyTicketRequest(serviceTicketRequest(request, response), request); - if (!continueFilterChain) { - super.successfulAuthentication(request, response, chain, authResult); - return; - } - this.logger.debug( - LogMessage.format("Authentication success. Updating SecurityContextHolder to contain: %s", authResult)); - - SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); - context.setAuthentication(authResult); - this.securityContextHolderStrategy.setContext(context); - this.securityContextRepository.saveContext(context, request, response); - if (this.eventPublisher != null) { - this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); - } - chain.doFilter(request, response); - } - - @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) - throws AuthenticationException, IOException { - // if the request is a proxy request process it and return null to indicate the - // request has been processed - if (proxyReceptorRequest(request)) { - this.logger.debug("Responding to proxy receptor request"); - WebUtils.readAndRespondToProxyReceptorRequest(request, response, this.proxyGrantingTicketStorage); - return null; - } - String serviceTicket = obtainArtifact(request); - if (serviceTicket == null) { - this.logger.debug("Failed to obtain an artifact (cas ticket)"); - serviceTicket = ""; - } - boolean serviceTicketRequest = serviceTicketRequest(request, response); - CasServiceTicketAuthenticationToken authRequest = serviceTicketRequest - ? CasServiceTicketAuthenticationToken.stateful(serviceTicket) - : CasServiceTicketAuthenticationToken.stateless(serviceTicket); - authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); - return this.getAuthenticationManager().authenticate(authRequest); - } - - /** - * If present, gets the artifact (CAS ticket) from the {@link HttpServletRequest}. - * @param request - * @return if present the artifact from the {@link HttpServletRequest}, else null - */ - protected String obtainArtifact(HttpServletRequest request) { - return request.getParameter(this.artifactParameter); - } - - /** - * Overridden to provide proxying capabilities. - */ - @Override - protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { - final boolean serviceTicketRequest = serviceTicketRequest(request, response); - final boolean result = serviceTicketRequest || proxyReceptorRequest(request) - || (proxyTicketRequest(serviceTicketRequest, request)); - if (this.logger.isDebugEnabled()) { - this.logger.debug("requiresAuthentication = " + result); - } - return result; - } - - /** - * Sets the {@link AuthenticationFailureHandler} for proxy requests. - * @param proxyFailureHandler - */ - public final void setProxyAuthenticationFailureHandler(AuthenticationFailureHandler proxyFailureHandler) { - Assert.notNull(proxyFailureHandler, "proxyFailureHandler cannot be null"); - this.proxyFailureHandler = proxyFailureHandler; - } - - /** - * Wraps the {@link AuthenticationFailureHandler} to distinguish between handling - * proxy ticket authentication failures and service ticket failures. - */ - @Override - public final void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) { - super.setAuthenticationFailureHandler(new CasAuthenticationFailureHandler(failureHandler)); - } - - public final void setProxyReceptorUrl(final String proxyReceptorUrl) { - this.proxyReceptorMatcher = new AntPathRequestMatcher("/**" + proxyReceptorUrl); - } - - public final void setProxyGrantingTicketStorage(final ProxyGrantingTicketStorage proxyGrantingTicketStorage) { - this.proxyGrantingTicketStorage = proxyGrantingTicketStorage; - } - - public final void setServiceProperties(final ServiceProperties serviceProperties) { - this.artifactParameter = serviceProperties.getArtifactParameter(); - this.authenticateAllArtifacts = serviceProperties.isAuthenticateAllArtifacts(); - } - - /** - * Indicates if the request is elgible to process a service ticket. This method exists - * for readability. - * @param request - * @param response - * @return - */ - private boolean serviceTicketRequest(HttpServletRequest request, HttpServletResponse response) { - boolean result = super.requiresAuthentication(request, response); - this.logger.debug(LogMessage.format("serviceTicketRequest = %s", result)); - return result; - } - - /** - * Indicates if the request is elgible to process a proxy ticket. - * @param request - * @return - */ - private boolean proxyTicketRequest(boolean serviceTicketRequest, HttpServletRequest request) { - if (serviceTicketRequest) { - return false; - } - boolean result = this.authenticateAllArtifacts && obtainArtifact(request) != null && !authenticated(); - this.logger.debug(LogMessage.format("proxyTicketRequest = %s", result)); - return result; - } - - /** - * Determines if a user is already authenticated. - * @return - */ - private boolean authenticated() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return authentication != null && authentication.isAuthenticated() - && !(authentication instanceof AnonymousAuthenticationToken); - } - - /** - * Indicates if the request is elgible to be processed as the proxy receptor. - * @param request - * @return - */ - private boolean proxyReceptorRequest(HttpServletRequest request) { - final boolean result = proxyReceptorConfigured() && this.proxyReceptorMatcher.matches(request); - this.logger.debug(LogMessage.format("proxyReceptorRequest = %s", result)); - return result; - } - - /** - * Determines if the {@link CasAuthenticationFilter} is configured to handle the proxy - * receptor requests. - * @return - */ - private boolean proxyReceptorConfigured() { - final boolean result = this.proxyGrantingTicketStorage != null && this.proxyReceptorMatcher != null; - this.logger.debug(LogMessage.format("proxyReceptorConfigured = %s", result)); - return result; - } - - /** - * A wrapper for the AuthenticationFailureHandler that will flex the - * {@link AuthenticationFailureHandler} that is used. The value - * {@link CasAuthenticationFilter#setProxyAuthenticationFailureHandler(AuthenticationFailureHandler)} - * will be used for proxy requests that fail. The value - * {@link CasAuthenticationFilter#setAuthenticationFailureHandler(AuthenticationFailureHandler)} - * will be used for service tickets that fail. - */ - private class CasAuthenticationFailureHandler implements AuthenticationFailureHandler { - - private final AuthenticationFailureHandler serviceTicketFailureHandler; - - CasAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) { - Assert.notNull(failureHandler, "failureHandler"); - this.serviceTicketFailureHandler = failureHandler; - } - - @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException, ServletException { - if (serviceTicketRequest(request, response)) { - this.serviceTicketFailureHandler.onAuthenticationFailure(request, response, exception); - } - else { - CasAuthenticationFilter.this.proxyFailureHandler.onAuthenticationFailure(request, response, exception); - } - } - - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetails.java b/cas/src/main/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetails.java deleted file mode 100644 index bc19183a79..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetails.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2011-2023 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.cas.web.authentication; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.regex.Pattern; - -import jakarta.servlet.http.HttpServletRequest; - -import org.springframework.security.web.authentication.WebAuthenticationDetails; -import org.springframework.security.web.util.UrlUtils; -import org.springframework.util.Assert; - -/** - * A default implementation of {@link ServiceAuthenticationDetails} that figures out the - * value for {@link #getServiceUrl()} by inspecting the current {@link HttpServletRequest} - * and using the current URL minus the artifact and the corresponding value. - * - * @author Rob Winch - */ -final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails - implements ServiceAuthenticationDetails { - - private static final long serialVersionUID = 6192409090610517700L; - - private final String serviceUrl; - - /** - * Creates a new instance - * @param request the current {@link HttpServletRequest} to obtain the - * {@link #getServiceUrl()} from. - * @param artifactPattern the {@link Pattern} that will be used to clean up the query - * string from containing the artifact name and value. This can be created using - * {@link #createArtifactPattern(String)}. - */ - DefaultServiceAuthenticationDetails(String casService, HttpServletRequest request, Pattern artifactPattern) - throws MalformedURLException { - super(request); - URL casServiceUrl = new URL(casService); - int port = getServicePort(casServiceUrl); - final String query = getQueryString(request, artifactPattern); - this.serviceUrl = UrlUtils.buildFullRequestUrl(casServiceUrl.getProtocol(), casServiceUrl.getHost(), port, - request.getRequestURI(), query); - } - - /** - * Returns the current URL minus the artifact parameter and its value, if present. - * @see org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails#getServiceUrl() - */ - @Override - public String getServiceUrl() { - return this.serviceUrl; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj) || !(obj instanceof DefaultServiceAuthenticationDetails)) { - return false; - } - ServiceAuthenticationDetails that = (ServiceAuthenticationDetails) obj; - return this.serviceUrl.equals(that.getServiceUrl()); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + this.serviceUrl.hashCode(); - return result; - } - - @Override - public String toString() { - StringBuilder result = new StringBuilder(); - result.append(super.toString()); - result.append("ServiceUrl: "); - result.append(this.serviceUrl); - return result.toString(); - } - - /** - * If present, removes the artifactParameterName and the corresponding value from the - * query String. - * @param request - * @return the query String minus the artifactParameterName and the corresponding - * value. - */ - private String getQueryString(final HttpServletRequest request, final Pattern artifactPattern) { - final String query = request.getQueryString(); - if (query == null) { - return null; - } - String result = artifactPattern.matcher(query).replaceFirst(""); - if (result.length() == 0) { - return null; - } - // strip off the trailing & only if the artifact was the first query param - return result.startsWith("&") ? result.substring(1) : result; - } - - /** - * Creates a {@link Pattern} that can be passed into the constructor. This allows the - * {@link Pattern} to be reused for every instance of - * {@link DefaultServiceAuthenticationDetails}. - * @param artifactParameterName - * @return - */ - static Pattern createArtifactPattern(String artifactParameterName) { - Assert.hasLength(artifactParameterName, "artifactParameterName is expected to have a length"); - return Pattern.compile("&?" + Pattern.quote(artifactParameterName) + "=[^&]*"); - } - - /** - * Gets the port from the casServiceURL ensuring to return the proper value if the - * default port is being used. - * @param casServiceUrl the casServerUrl to be used (i.e. - * "https://example.com/context/login/cas") - * @return the port that is configured for the casServerUrl - */ - private static int getServicePort(URL casServiceUrl) { - int port = casServiceUrl.getPort(); - if (port == -1) { - port = casServiceUrl.getDefaultPort(); - } - return port; - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetails.java b/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetails.java deleted file mode 100644 index 9c39d66544..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetails.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2011-2023 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.cas.web.authentication; - -import java.io.Serializable; - -import org.springframework.security.cas.ServiceProperties; -import org.springframework.security.cas.authentication.CasAuthenticationProvider; -import org.springframework.security.core.Authentication; - -/** - * In order for the {@link CasAuthenticationProvider} to provide the correct service url - * to authenticate the ticket, the returned value of {@link Authentication#getDetails()} - * should implement this interface when tickets can be sent to any URL rather than only - * {@link ServiceProperties#getService()}. - * - * @author Rob Winch - * @see ServiceAuthenticationDetailsSource - */ -public interface ServiceAuthenticationDetails extends Serializable { - - /** - * Gets the absolute service url (i.e. https://example.com/service/). - * @return the service url. Cannot be null. - */ - String getServiceUrl(); - -} diff --git a/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetailsSource.java b/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetailsSource.java deleted file mode 100644 index a623a45434..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/web/authentication/ServiceAuthenticationDetailsSource.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2011-2023 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.cas.web.authentication; - -import java.net.MalformedURLException; -import java.util.regex.Pattern; - -import jakarta.servlet.http.HttpServletRequest; - -import org.springframework.security.authentication.AuthenticationDetailsSource; -import org.springframework.security.cas.ServiceProperties; -import org.springframework.util.Assert; - -/** - * The {@code AuthenticationDetailsSource} that is set on the - * {@code CasAuthenticationFilter} should return a value that implements - * {@code ServiceAuthenticationDetails} if the application needs to authenticate dynamic - * service urls. The - * {@code ServiceAuthenticationDetailsSource#buildDetails(HttpServletRequest)} creates a - * default {@code ServiceAuthenticationDetails}. - * - * @author Rob Winch - */ -public class ServiceAuthenticationDetailsSource - implements AuthenticationDetailsSource { - - private final Pattern artifactPattern; - - private ServiceProperties serviceProperties; - - /** - * Creates an implementation that uses the specified ServiceProperties and the default - * CAS artifactParameterName. - * @param serviceProperties The ServiceProperties to use to construct the serviceUrl. - */ - public ServiceAuthenticationDetailsSource(ServiceProperties serviceProperties) { - this(serviceProperties, ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER); - } - - /** - * Creates an implementation that uses the specified artifactParameterName - * @param serviceProperties The ServiceProperties to use to construct the serviceUrl. - * @param artifactParameterName the artifactParameterName that is removed from the - * current URL. The result becomes the service url. Cannot be null and cannot be an - * empty String. - */ - public ServiceAuthenticationDetailsSource(ServiceProperties serviceProperties, String artifactParameterName) { - Assert.notNull(serviceProperties, "serviceProperties cannot be null"); - this.serviceProperties = serviceProperties; - this.artifactPattern = DefaultServiceAuthenticationDetails.createArtifactPattern(artifactParameterName); - } - - /** - * @param context the {@code HttpServletRequest} object. - * @return the {@code ServiceAuthenticationDetails} containing information about the - * current request - */ - @Override - public ServiceAuthenticationDetails buildDetails(HttpServletRequest context) { - try { - return new DefaultServiceAuthenticationDetails(this.serviceProperties.getService(), context, - this.artifactPattern); - } - catch (MalformedURLException ex) { - throw new RuntimeException(ex); - } - } - -} diff --git a/cas/src/main/java/org/springframework/security/cas/web/authentication/package-info.java b/cas/src/main/java/org/springframework/security/cas/web/authentication/package-info.java deleted file mode 100644 index ebfcddce91..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/web/authentication/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2002-2023 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. - */ - -/** - * Authentication processing mechanisms which respond to the submission of authentication - * credentials using CAS. - */ -package org.springframework.security.cas.web.authentication; diff --git a/cas/src/main/java/org/springframework/security/cas/web/package-info.java b/cas/src/main/java/org/springframework/security/cas/web/package-info.java deleted file mode 100644 index ecba987b1d..0000000000 --- a/cas/src/main/java/org/springframework/security/cas/web/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2002-2023 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. - */ - -/** - * Authenticates standard web browser users via CAS. - */ -package org.springframework.security.cas.web; diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/AbstractStatelessTicketCacheTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/AbstractStatelessTicketCacheTests.java deleted file mode 100644 index a337eac47b..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/authentication/AbstractStatelessTicketCacheTests.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2002-2023 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.cas.authentication; - -import java.util.ArrayList; -import java.util.List; - -import org.apereo.cas.client.validation.Assertion; -import org.apereo.cas.client.validation.AssertionImpl; - -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.userdetails.User; - -/** - * @author Scott Battaglia - * @since 2.0 - * - */ -public abstract class AbstractStatelessTicketCacheTests { - - protected CasAuthenticationToken getToken() { - List proxyList = new ArrayList<>(); - proxyList.add("https://localhost/newPortal/login/cas"); - User user = new User("rod", "password", true, true, true, true, - AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); - final Assertion assertion = new AssertionImpl("rod"); - return new CasAuthenticationToken("key", user, "ST-0-ER94xMJmn6pha35CQRoZ", - AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO"), user, assertion); - } - -} diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java deleted file mode 100644 index f11f191507..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java +++ /dev/null @@ -1,376 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas.authentication; - -import java.util.HashMap; -import java.util.Map; - -import org.apereo.cas.client.validation.Assertion; -import org.apereo.cas.client.validation.AssertionImpl; -import org.apereo.cas.client.validation.TicketValidator; -import org.junit.jupiter.api.Test; - -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.cas.ServiceProperties; -import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.web.authentication.WebAuthenticationDetails; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -/** - * Tests {@link CasAuthenticationProvider}. - * - * @author Ben Alex - * @author Scott Battaglia - */ -@SuppressWarnings("unchecked") -public class CasAuthenticationProviderTests { - - private UserDetails makeUserDetails() { - return new User("user", "password", true, true, true, true, - AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); - } - - private UserDetails makeUserDetailsFromAuthoritiesPopulator() { - return new User("user", "password", true, true, true, true, - AuthorityUtils.createAuthorityList("ROLE_A", "ROLE_B")); - } - - private ServiceProperties makeServiceProperties() { - final ServiceProperties serviceProperties = new ServiceProperties(); - serviceProperties.setSendRenew(false); - serviceProperties.setService("http://test.com"); - return serviceProperties; - } - - @Test - public void statefulAuthenticationIsSuccessful() throws Exception { - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); - cap.setKey("qwerty"); - StatelessTicketCache cache = new MockStatelessTicketCache(); - cap.setStatelessTicketCache(cache); - cap.setServiceProperties(makeServiceProperties()); - cap.setTicketValidator(new MockTicketValidator(true)); - cap.afterPropertiesSet(); - CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateful("ST-123"); - token.setDetails("details"); - Authentication result = cap.authenticate(token); - // Confirm ST-123 was NOT added to the cache - assertThat(cache.getByTicketId("ST-456") == null).isTrue(); - if (!(result instanceof CasAuthenticationToken)) { - fail("Should have returned a CasAuthenticationToken"); - } - CasAuthenticationToken casResult = (CasAuthenticationToken) result; - assertThat(casResult.getPrincipal()).isEqualTo(makeUserDetailsFromAuthoritiesPopulator()); - assertThat(casResult.getCredentials()).isEqualTo("ST-123"); - assertThat(casResult.getAuthorities()).contains(new SimpleGrantedAuthority("ROLE_A")); - assertThat(casResult.getAuthorities()).contains(new SimpleGrantedAuthority("ROLE_B")); - assertThat(casResult.getKeyHash()).isEqualTo(cap.getKey().hashCode()); - assertThat(casResult.getDetails()).isEqualTo("details"); - // Now confirm the CasAuthenticationToken is automatically re-accepted. - // To ensure TicketValidator not called again, set it to deliver an exception... - cap.setTicketValidator(new MockTicketValidator(false)); - Authentication laterResult = cap.authenticate(result); - assertThat(laterResult).isEqualTo(result); - } - - @Test - public void statelessAuthenticationIsSuccessful() throws Exception { - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); - cap.setKey("qwerty"); - StatelessTicketCache cache = new MockStatelessTicketCache(); - cap.setStatelessTicketCache(cache); - cap.setTicketValidator(new MockTicketValidator(true)); - cap.setServiceProperties(makeServiceProperties()); - cap.afterPropertiesSet(); - CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless("ST-456"); - token.setDetails("details"); - Authentication result = cap.authenticate(token); - // Confirm ST-456 was added to the cache - assertThat(cache.getByTicketId("ST-456") != null).isTrue(); - if (!(result instanceof CasAuthenticationToken)) { - fail("Should have returned a CasAuthenticationToken"); - } - assertThat(result.getPrincipal()).isEqualTo(makeUserDetailsFromAuthoritiesPopulator()); - assertThat(result.getCredentials()).isEqualTo("ST-456"); - assertThat(result.getDetails()).isEqualTo("details"); - // Now try to authenticate again. To ensure TicketValidator not - // called again, set it to deliver an exception... - cap.setTicketValidator(new MockTicketValidator(false)); - // Previously created CasServiceTicketAuthenticationToken is OK - Authentication newResult = cap.authenticate(token); - assertThat(newResult.getPrincipal()).isEqualTo(makeUserDetailsFromAuthoritiesPopulator()); - assertThat(newResult.getCredentials()).isEqualTo("ST-456"); - } - - @Test - public void authenticateAllNullService() throws Exception { - String serviceUrl = "https://service/context"; - ServiceAuthenticationDetails details = mock(ServiceAuthenticationDetails.class); - given(details.getServiceUrl()).willReturn(serviceUrl); - TicketValidator validator = mock(TicketValidator.class); - given(validator.validate(any(String.class), any(String.class))).willReturn(new AssertionImpl("rod")); - ServiceProperties serviceProperties = makeServiceProperties(); - serviceProperties.setAuthenticateAllArtifacts(true); - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); - cap.setKey("qwerty"); - cap.setTicketValidator(validator); - cap.setServiceProperties(serviceProperties); - cap.afterPropertiesSet(); - String ticket = "ST-456"; - CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless(ticket); - Authentication result = cap.authenticate(token); - } - - @Test - public void authenticateAllAuthenticationIsSuccessful() throws Exception { - String serviceUrl = "https://service/context"; - ServiceAuthenticationDetails details = mock(ServiceAuthenticationDetails.class); - given(details.getServiceUrl()).willReturn(serviceUrl); - TicketValidator validator = mock(TicketValidator.class); - given(validator.validate(any(String.class), any(String.class))).willReturn(new AssertionImpl("rod")); - ServiceProperties serviceProperties = makeServiceProperties(); - serviceProperties.setAuthenticateAllArtifacts(true); - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); - cap.setKey("qwerty"); - cap.setTicketValidator(validator); - cap.setServiceProperties(serviceProperties); - cap.afterPropertiesSet(); - String ticket = "ST-456"; - CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless(ticket); - Authentication result = cap.authenticate(token); - verify(validator).validate(ticket, serviceProperties.getService()); - serviceProperties.setAuthenticateAllArtifacts(true); - result = cap.authenticate(token); - verify(validator, times(2)).validate(ticket, serviceProperties.getService()); - token.setDetails(details); - result = cap.authenticate(token); - verify(validator).validate(ticket, serviceUrl); - serviceProperties.setAuthenticateAllArtifacts(false); - serviceProperties.setService(null); - cap.setServiceProperties(serviceProperties); - cap.afterPropertiesSet(); - result = cap.authenticate(token); - verify(validator, times(2)).validate(ticket, serviceUrl); - token.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest())); - assertThatIllegalStateException().isThrownBy(() -> cap.authenticate(token)); - cap.setServiceProperties(null); - cap.afterPropertiesSet(); - assertThatIllegalStateException().isThrownBy(() -> cap.authenticate(token)); - } - - @Test - public void missingTicketIdIsDetected() throws Exception { - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); - cap.setKey("qwerty"); - StatelessTicketCache cache = new MockStatelessTicketCache(); - cap.setStatelessTicketCache(cache); - cap.setTicketValidator(new MockTicketValidator(true)); - cap.setServiceProperties(makeServiceProperties()); - cap.afterPropertiesSet(); - CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateful(""); - assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> cap.authenticate(token)); - } - - @Test - public void invalidKeyIsDetected() throws Exception { - final Assertion assertion = new AssertionImpl("test"); - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); - cap.setKey("qwerty"); - StatelessTicketCache cache = new MockStatelessTicketCache(); - cap.setStatelessTicketCache(cache); - cap.setTicketValidator(new MockTicketValidator(true)); - cap.setServiceProperties(makeServiceProperties()); - cap.afterPropertiesSet(); - CasAuthenticationToken token = new CasAuthenticationToken("WRONG_KEY", makeUserDetails(), "credentials", - AuthorityUtils.createAuthorityList("XX"), makeUserDetails(), assertion); - assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> cap.authenticate(token)); - } - - @Test - public void detectsMissingAuthoritiesPopulator() throws Exception { - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - cap.setKey("qwerty"); - cap.setStatelessTicketCache(new MockStatelessTicketCache()); - cap.setTicketValidator(new MockTicketValidator(true)); - cap.setServiceProperties(makeServiceProperties()); - assertThatIllegalArgumentException().isThrownBy(() -> cap.afterPropertiesSet()); - } - - @Test - public void detectsMissingKey() throws Exception { - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); - cap.setStatelessTicketCache(new MockStatelessTicketCache()); - cap.setTicketValidator(new MockTicketValidator(true)); - cap.setServiceProperties(makeServiceProperties()); - assertThatIllegalArgumentException().isThrownBy(() -> cap.afterPropertiesSet()); - } - - @Test - public void detectsMissingStatelessTicketCache() throws Exception { - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - // set this explicitly to null to test failure - cap.setStatelessTicketCache(null); - cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); - cap.setKey("qwerty"); - cap.setTicketValidator(new MockTicketValidator(true)); - cap.setServiceProperties(makeServiceProperties()); - assertThatIllegalArgumentException().isThrownBy(() -> cap.afterPropertiesSet()); - } - - @Test - public void detectsMissingTicketValidator() throws Exception { - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); - cap.setKey("qwerty"); - cap.setStatelessTicketCache(new MockStatelessTicketCache()); - cap.setServiceProperties(makeServiceProperties()); - assertThatIllegalArgumentException().isThrownBy(() -> cap.afterPropertiesSet()); - } - - @Test - public void gettersAndSettersMatch() throws Exception { - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); - cap.setKey("qwerty"); - cap.setStatelessTicketCache(new MockStatelessTicketCache()); - cap.setTicketValidator(new MockTicketValidator(true)); - cap.setServiceProperties(makeServiceProperties()); - cap.afterPropertiesSet(); - // TODO disabled because why do we need to expose this? - // assertThat(cap.getUserDetailsService() != null).isTrue(); - assertThat(cap.getKey()).isEqualTo("qwerty"); - assertThat(cap.getStatelessTicketCache() != null).isTrue(); - assertThat(cap.getTicketValidator() != null).isTrue(); - } - - @Test - public void ignoresClassesItDoesNotSupport() throws Exception { - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); - cap.setKey("qwerty"); - cap.setStatelessTicketCache(new MockStatelessTicketCache()); - cap.setTicketValidator(new MockTicketValidator(true)); - cap.setServiceProperties(makeServiceProperties()); - cap.afterPropertiesSet(); - TestingAuthenticationToken token = new TestingAuthenticationToken("user", "password", "ROLE_A"); - assertThat(cap.supports(TestingAuthenticationToken.class)).isFalse(); - // Try it anyway - assertThat(cap.authenticate(token)).isNull(); - } - - @Test - public void ignoresUsernamePasswordAuthenticationTokensWithoutCasIdentifiersAsPrincipal() throws Exception { - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); - cap.setKey("qwerty"); - cap.setStatelessTicketCache(new MockStatelessTicketCache()); - cap.setTicketValidator(new MockTicketValidator(true)); - cap.setServiceProperties(makeServiceProperties()); - cap.afterPropertiesSet(); - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("some_normal_user", - "password", AuthorityUtils.createAuthorityList("ROLE_A")); - assertThat(cap.authenticate(token)).isNull(); - } - - @Test - public void supportsRequiredTokens() { - CasAuthenticationProvider cap = new CasAuthenticationProvider(); - assertThat(cap.supports(CasServiceTicketAuthenticationToken.class)).isTrue(); - assertThat(cap.supports(CasAuthenticationToken.class)).isTrue(); - } - - private class MockAuthoritiesPopulator implements AuthenticationUserDetailsService { - - @Override - public UserDetails loadUserDetails(final Authentication token) throws UsernameNotFoundException { - return makeUserDetailsFromAuthoritiesPopulator(); - } - - } - - private class MockStatelessTicketCache implements StatelessTicketCache { - - private Map cache = new HashMap<>(); - - @Override - public CasAuthenticationToken getByTicketId(String serviceTicket) { - return this.cache.get(serviceTicket); - } - - @Override - public void putTicketInCache(CasAuthenticationToken token) { - this.cache.put(token.getCredentials().toString(), token); - } - - @Override - public void removeTicketFromCache(CasAuthenticationToken token) { - throw new UnsupportedOperationException("mock method not implemented"); - } - - @Override - public void removeTicketFromCache(String serviceTicket) { - throw new UnsupportedOperationException("mock method not implemented"); - } - - } - - private class MockTicketValidator implements TicketValidator { - - private boolean returnTicket; - - MockTicketValidator(boolean returnTicket) { - this.returnTicket = returnTicket; - } - - @Override - public Assertion validate(final String ticket, final String service) { - if (this.returnTicket) { - return new AssertionImpl("rod"); - } - throw new BadCredentialsException("As requested from mock"); - } - - } - -} diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java deleted file mode 100644 index aa0048d349..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas.authentication; - -import java.util.Collections; -import java.util.List; - -import org.apereo.cas.client.validation.Assertion; -import org.apereo.cas.client.validation.AssertionImpl; -import org.junit.jupiter.api.Test; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * Tests {@link CasAuthenticationToken}. - * - * @author Ben Alex - */ -public class CasAuthenticationTokenTests { - - private final List ROLES = AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO"); - - private UserDetails makeUserDetails() { - return makeUserDetails("user"); - } - - private UserDetails makeUserDetails(final String name) { - return new User(name, "password", true, true, true, true, this.ROLES); - } - - @Test - public void testConstructorRejectsNulls() { - Assertion assertion = new AssertionImpl("test"); - assertThatIllegalArgumentException().isThrownBy(() -> new CasAuthenticationToken(null, makeUserDetails(), - "Password", this.ROLES, makeUserDetails(), assertion)); - assertThatIllegalArgumentException().isThrownBy( - () -> new CasAuthenticationToken("key", null, "Password", this.ROLES, makeUserDetails(), assertion)); - assertThatIllegalArgumentException().isThrownBy(() -> new CasAuthenticationToken("key", makeUserDetails(), null, - this.ROLES, makeUserDetails(), assertion)); - assertThatIllegalArgumentException().isThrownBy(() -> new CasAuthenticationToken("key", makeUserDetails(), - "Password", this.ROLES, makeUserDetails(), null)); - assertThatIllegalArgumentException().isThrownBy( - () -> new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, null, assertion)); - assertThatIllegalArgumentException().isThrownBy(() -> new CasAuthenticationToken("key", makeUserDetails(), - "Password", AuthorityUtils.createAuthorityList("ROLE_1", null), makeUserDetails(), assertion)); - } - - @Test - public void constructorWhenEmptyKeyThenThrowsException() { - assertThatIllegalArgumentException().isThrownBy( - () -> new CasAuthenticationToken("", "user", "password", Collections.emptyList(), - new User("user", "password", Collections.emptyList()), null)); - } - - @Test - public void testEqualsWhenEqual() { - final Assertion assertion = new AssertionImpl("test"); - CasAuthenticationToken token1 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, - makeUserDetails(), assertion); - CasAuthenticationToken token2 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, - makeUserDetails(), assertion); - assertThat(token2).isEqualTo(token1); - } - - @Test - public void testGetters() { - // Build the proxy list returned in the ticket from CAS - final Assertion assertion = new AssertionImpl("test"); - CasAuthenticationToken token = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, - makeUserDetails(), assertion); - assertThat(token.getKeyHash()).isEqualTo("key".hashCode()); - assertThat(token.getPrincipal()).isEqualTo(makeUserDetails()); - assertThat(token.getCredentials()).isEqualTo("Password"); - assertThat(token.getAuthorities()).contains(new SimpleGrantedAuthority("ROLE_ONE")); - assertThat(token.getAuthorities()).contains(new SimpleGrantedAuthority("ROLE_TWO")); - assertThat(token.getAssertion()).isEqualTo(assertion); - assertThat(token.getUserDetails().getUsername()).isEqualTo(makeUserDetails().getUsername()); - } - - @Test - public void testNoArgConstructorDoesntExist() { - assertThatExceptionOfType(NoSuchMethodException.class) - .isThrownBy(() -> CasAuthenticationToken.class.getDeclaredConstructor((Class[]) null)); - } - - @Test - public void testNotEqualsDueToAbstractParentEqualsCheck() { - final Assertion assertion = new AssertionImpl("test"); - CasAuthenticationToken token1 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, - makeUserDetails(), assertion); - CasAuthenticationToken token2 = new CasAuthenticationToken("key", makeUserDetails("OTHER_NAME"), "Password", - this.ROLES, makeUserDetails(), assertion); - assertThat(!token1.equals(token2)).isTrue(); - } - - @Test - public void testNotEqualsDueToKey() { - final Assertion assertion = new AssertionImpl("test"); - CasAuthenticationToken token1 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, - makeUserDetails(), assertion); - CasAuthenticationToken token2 = new CasAuthenticationToken("DIFFERENT_KEY", makeUserDetails(), "Password", - this.ROLES, makeUserDetails(), assertion); - assertThat(!token1.equals(token2)).isTrue(); - } - - @Test - public void testNotEqualsDueToAssertion() { - final Assertion assertion = new AssertionImpl("test"); - final Assertion assertion2 = new AssertionImpl("test"); - CasAuthenticationToken token1 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, - makeUserDetails(), assertion); - CasAuthenticationToken token2 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, - makeUserDetails(), assertion2); - assertThat(!token1.equals(token2)).isTrue(); - } - - @Test - public void testSetAuthenticated() { - final Assertion assertion = new AssertionImpl("test"); - CasAuthenticationToken token = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, - makeUserDetails(), assertion); - assertThat(token.isAuthenticated()).isTrue(); - token.setAuthenticated(false); - assertThat(!token.isAuthenticated()).isTrue(); - } - - @Test - public void testToString() { - final Assertion assertion = new AssertionImpl("test"); - CasAuthenticationToken token = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, - makeUserDetails(), assertion); - String result = token.toString(); - assertThat(result.lastIndexOf("Credentials (Service/Proxy Ticket):") != -1).isTrue(); - } - -} diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/NullStatelessTicketCacheTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/NullStatelessTicketCacheTests.java deleted file mode 100644 index f5a87e5c4e..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/authentication/NullStatelessTicketCacheTests.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas.authentication; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Test cases for the @link {@link NullStatelessTicketCache} - * - * @author Scott Battaglia - * - */ -public class NullStatelessTicketCacheTests extends AbstractStatelessTicketCacheTests { - - private StatelessTicketCache cache = new NullStatelessTicketCache(); - - @Test - public void testGetter() { - assertThat(this.cache.getByTicketId(null)).isNull(); - assertThat(this.cache.getByTicketId("test")).isNull(); - } - - @Test - public void testInsertAndGet() { - final CasAuthenticationToken token = getToken(); - this.cache.putTicketInCache(token); - assertThat(this.cache.getByTicketId((String) token.getCredentials())).isNull(); - } - -} diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCacheTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCacheTests.java deleted file mode 100644 index e27344a9ce..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/authentication/SpringCacheBasedTicketCacheTests.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas.authentication; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.cache.CacheManager; -import org.springframework.cache.concurrent.ConcurrentMapCacheManager; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * Tests - * {@link org.springframework.security.cas.authentication.SpringCacheBasedTicketCache}. - * - * @author Marten Deinum - * @since 3.2 - */ -public class SpringCacheBasedTicketCacheTests extends AbstractStatelessTicketCacheTests { - - private static CacheManager cacheManager; - - @BeforeAll - public static void initCacheManaer() { - cacheManager = new ConcurrentMapCacheManager(); - cacheManager.getCache("castickets"); - } - - @Test - public void testCacheOperation() throws Exception { - SpringCacheBasedTicketCache cache = new SpringCacheBasedTicketCache(cacheManager.getCache("castickets")); - final CasAuthenticationToken token = getToken(); - // Check it gets stored in the cache - cache.putTicketInCache(token); - assertThat(cache.getByTicketId("ST-0-ER94xMJmn6pha35CQRoZ")).isEqualTo(token); - // Check it gets removed from the cache - cache.removeTicketFromCache(getToken()); - assertThat(cache.getByTicketId("ST-0-ER94xMJmn6pha35CQRoZ")).isNull(); - // Check it doesn't return values for null or unknown service tickets - assertThat(cache.getByTicketId(null)).isNull(); - assertThat(cache.getByTicketId("UNKNOWN_SERVICE_TICKET")).isNull(); - } - - @Test - public void testStartupDetectsMissingCache() throws Exception { - assertThatIllegalArgumentException().isThrownBy(() -> new SpringCacheBasedTicketCache(null)); - } - -} diff --git a/cas/src/test/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixinTests.java b/cas/src/test/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixinTests.java deleted file mode 100644 index 64d764291c..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/jackson2/CasAuthenticationTokenMixinTests.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2015-2023 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.cas.jackson2; - -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apereo.cas.client.authentication.AttributePrincipalImpl; -import org.apereo.cas.client.validation.Assertion; -import org.apereo.cas.client.validation.AssertionImpl; -import org.json.JSONException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; - -import org.springframework.security.cas.authentication.CasAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.jackson2.SecurityJackson2Modules; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Jitendra Singh - * @since 4.2 - */ -public class CasAuthenticationTokenMixinTests { - - private static final String KEY = "casKey"; - - private static final String PASSWORD = "\"1234\""; - - private static final Date START_DATE = new Date(); - - private static final Date END_DATE = new Date(); - - public static final String AUTHORITY_JSON = "{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"authority\": \"ROLE_USER\"}"; - - public static final String AUTHORITIES_SET_JSON = "[\"java.util.Collections$UnmodifiableSet\", [" + AUTHORITY_JSON - + "]]"; - - public static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", [" - + AUTHORITY_JSON + "]]"; - - // @formatter:off - public static final String USER_JSON = "{" - + "\"@class\": \"org.springframework.security.core.userdetails.User\", " - + "\"username\": \"admin\"," - + " \"password\": " + PASSWORD + ", " - + "\"accountNonExpired\": true, " - + "\"accountNonLocked\": true, " - + "\"credentialsNonExpired\": true, " - + "\"enabled\": true, " - + "\"authorities\": " + AUTHORITIES_SET_JSON - + "}"; - // @formatter:on - private static final String CAS_TOKEN_JSON = "{" - + "\"@class\": \"org.springframework.security.cas.authentication.CasAuthenticationToken\", " - + "\"keyHash\": " + KEY.hashCode() + "," + "\"principal\": " + USER_JSON + ", " + "\"credentials\": " - + PASSWORD + ", " + "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + "," + "\"userDetails\": " + USER_JSON - + "," + "\"authenticated\": true, " + "\"details\": null," + "\"assertion\": {" - + "\"@class\": \"org.apereo.cas.client.validation.AssertionImpl\", " + "\"principal\": {" - + "\"@class\": \"org.apereo.cas.client.authentication.AttributePrincipalImpl\", " - + "\"name\": \"assertName\", " + "\"attributes\": {\"@class\": \"java.util.Collections$EmptyMap\"}, " - + "\"proxyGrantingTicket\": null, " + "\"proxyRetriever\": null" + "}, " - + "\"validFromDate\": [\"java.util.Date\", " + START_DATE.getTime() + "], " - + "\"validUntilDate\": [\"java.util.Date\", " + END_DATE.getTime() + "]," - + "\"authenticationDate\": [\"java.util.Date\", " + START_DATE.getTime() + "], " - + "\"attributes\": {\"@class\": \"java.util.Collections$EmptyMap\"}," - + "\"context\": {\"@class\":\"java.util.HashMap\"}" + "}" + "}"; - - private static final String CAS_TOKEN_CLEARED_JSON = CAS_TOKEN_JSON.replaceFirst(PASSWORD, "null"); - - protected ObjectMapper mapper; - - @BeforeEach - public void setup() { - this.mapper = new ObjectMapper(); - ClassLoader loader = getClass().getClassLoader(); - this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); - } - - @Test - public void serializeCasAuthenticationTest() throws JsonProcessingException, JSONException { - CasAuthenticationToken token = createCasAuthenticationToken(); - String actualJson = this.mapper.writeValueAsString(token); - JSONAssert.assertEquals(CAS_TOKEN_JSON, actualJson, true); - } - - @Test - public void serializeCasAuthenticationTestAfterEraseCredentialInvoked() - throws JsonProcessingException, JSONException { - CasAuthenticationToken token = createCasAuthenticationToken(); - token.eraseCredentials(); - String actualJson = this.mapper.writeValueAsString(token); - JSONAssert.assertEquals(CAS_TOKEN_CLEARED_JSON, actualJson, true); - } - - @Test - public void deserializeCasAuthenticationTestAfterEraseCredentialInvoked() throws Exception { - CasAuthenticationToken token = this.mapper.readValue(CAS_TOKEN_CLEARED_JSON, CasAuthenticationToken.class); - assertThat(((UserDetails) token.getPrincipal()).getPassword()).isNull(); - } - - @Test - public void deserializeCasAuthenticationTest() throws IOException { - CasAuthenticationToken token = this.mapper.readValue(CAS_TOKEN_JSON, CasAuthenticationToken.class); - assertThat(token).isNotNull(); - assertThat(token.getPrincipal()).isNotNull().isInstanceOf(User.class); - assertThat(((User) token.getPrincipal()).getUsername()).isEqualTo("admin"); - assertThat(((User) token.getPrincipal()).getPassword()).isEqualTo("1234"); - assertThat(token.getUserDetails()).isNotNull().isInstanceOf(User.class); - assertThat(token.getAssertion()).isNotNull().isInstanceOf(AssertionImpl.class); - assertThat(token.getKeyHash()).isEqualTo(KEY.hashCode()); - assertThat(token.getUserDetails().getAuthorities()).extracting(GrantedAuthority::getAuthority) - .containsOnly("ROLE_USER"); - assertThat(token.getAssertion().getAuthenticationDate()).isEqualTo(START_DATE); - assertThat(token.getAssertion().getValidFromDate()).isEqualTo(START_DATE); - assertThat(token.getAssertion().getValidUntilDate()).isEqualTo(END_DATE); - assertThat(token.getAssertion().getPrincipal().getName()).isEqualTo("assertName"); - assertThat(token.getAssertion().getAttributes()).hasSize(0); - } - - private CasAuthenticationToken createCasAuthenticationToken() { - User principal = new User("admin", "1234", Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))); - Collection authorities = Collections - .singletonList(new SimpleGrantedAuthority("ROLE_USER")); - Assertion assertion = new AssertionImpl(new AttributePrincipalImpl("assertName"), START_DATE, END_DATE, - START_DATE, Collections.emptyMap()); - return new CasAuthenticationToken(KEY, principal, principal.getPassword(), authorities, - new User("admin", "1234", authorities), assertion); - } - -} diff --git a/cas/src/test/java/org/springframework/security/cas/userdetails/GrantedAuthorityFromAssertionAttributesUserDetailsServiceTests.java b/cas/src/test/java/org/springframework/security/cas/userdetails/GrantedAuthorityFromAssertionAttributesUserDetailsServiceTests.java deleted file mode 100644 index 5814d65851..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/userdetails/GrantedAuthorityFromAssertionAttributesUserDetailsServiceTests.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2002-2023 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.cas.userdetails; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import org.apereo.cas.client.authentication.AttributePrincipal; -import org.apereo.cas.client.validation.Assertion; -import org.junit.jupiter.api.Test; - -import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.userdetails.UserDetails; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * @author Luke Taylor - */ -public class GrantedAuthorityFromAssertionAttributesUserDetailsServiceTests { - - @Test - public void correctlyExtractsNamedAttributesFromAssertionAndConvertsThemToAuthorities() { - GrantedAuthorityFromAssertionAttributesUserDetailsService uds = new GrantedAuthorityFromAssertionAttributesUserDetailsService( - new String[] { "a", "b", "c", "d" }); - uds.setConvertToUpperCase(false); - Assertion assertion = mock(Assertion.class); - AttributePrincipal principal = mock(AttributePrincipal.class); - Map attributes = new HashMap<>(); - attributes.put("a", Arrays.asList("role_a1", "role_a2")); - attributes.put("b", "role_b"); - attributes.put("c", "role_c"); - attributes.put("d", null); - attributes.put("someother", "unused"); - given(assertion.getPrincipal()).willReturn(principal); - given(principal.getAttributes()).willReturn(attributes); - given(principal.getName()).willReturn("somebody"); - CasAssertionAuthenticationToken token = new CasAssertionAuthenticationToken(assertion, "ticket"); - UserDetails user = uds.loadUserDetails(token); - Set roles = AuthorityUtils.authorityListToSet(user.getAuthorities()); - assertThat(roles).containsExactlyInAnyOrder("role_a1", "role_a2", "role_b", "role_c"); - } - -} diff --git a/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationEntryPointTests.java b/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationEntryPointTests.java deleted file mode 100644 index 3720bf5718..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationEntryPointTests.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas.web; - -import java.net.URLEncoder; - -import org.junit.jupiter.api.Test; - -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.cas.ServiceProperties; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * Tests {@link CasAuthenticationEntryPoint}. - * - * @author Ben Alex - */ -public class CasAuthenticationEntryPointTests { - - @Test - public void testDetectsMissingLoginFormUrl() throws Exception { - CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint(); - ep.setServiceProperties(new ServiceProperties()); - assertThatIllegalArgumentException().isThrownBy(ep::afterPropertiesSet) - .withMessage("loginUrl must be specified"); - } - - @Test - public void testDetectsMissingServiceProperties() throws Exception { - CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint(); - ep.setLoginUrl("https://cas/login"); - assertThatIllegalArgumentException().isThrownBy(ep::afterPropertiesSet) - .withMessage("serviceProperties must be specified"); - } - - @Test - public void testGettersSetters() { - CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint(); - ep.setLoginUrl("https://cas/login"); - assertThat(ep.getLoginUrl()).isEqualTo("https://cas/login"); - ep.setServiceProperties(new ServiceProperties()); - assertThat(ep.getServiceProperties() != null).isTrue(); - } - - @Test - public void testNormalOperationWithRenewFalse() throws Exception { - ServiceProperties sp = new ServiceProperties(); - sp.setSendRenew(false); - sp.setService("https://mycompany.com/bigWebApp/login/cas"); - CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint(); - ep.setLoginUrl("https://cas/login"); - ep.setServiceProperties(sp); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/some_path"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ep.afterPropertiesSet(); - ep.commence(request, response, null); - assertThat( - "https://cas/login?service=" + URLEncoder.encode("https://mycompany.com/bigWebApp/login/cas", "UTF-8")) - .isEqualTo(response.getRedirectedUrl()); - } - - @Test - public void testNormalOperationWithRenewTrue() throws Exception { - ServiceProperties sp = new ServiceProperties(); - sp.setSendRenew(true); - sp.setService("https://mycompany.com/bigWebApp/login/cas"); - CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint(); - ep.setLoginUrl("https://cas/login"); - ep.setServiceProperties(sp); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestURI("/some_path"); - MockHttpServletResponse response = new MockHttpServletResponse(); - ep.afterPropertiesSet(); - ep.commence(request, response, null); - assertThat("https://cas/login?service=" - + URLEncoder.encode("https://mycompany.com/bigWebApp/login/cas", "UTF-8") + "&renew=true") - .isEqualTo(response.getRedirectedUrl()); - } - -} diff --git a/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java b/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java deleted file mode 100644 index 46689b42e9..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright 2002-2023 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.cas.web; - -import jakarta.servlet.FilterChain; -import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -import org.springframework.mock.web.MockFilterChain; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.cas.ServiceProperties; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -import org.springframework.security.web.context.SecurityContextRepository; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -/** - * Tests {@link CasAuthenticationFilter}. - * - * @author Ben Alex - * @author Rob Winch - */ -public class CasAuthenticationFilterTests { - - @AfterEach - public void tearDown() { - SecurityContextHolder.clearContext(); - } - - @Test - public void testGettersSetters() { - CasAuthenticationFilter filter = new CasAuthenticationFilter(); - filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class)); - filter.setProxyReceptorUrl("/someurl"); - filter.setServiceProperties(new ServiceProperties()); - } - - @Test - public void testNormalOperation() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setServletPath("/login/cas"); - request.addParameter("ticket", "ST-0-ER94xMJmn6pha35CQRoZ"); - CasAuthenticationFilter filter = new CasAuthenticationFilter(); - filter.setAuthenticationManager((a) -> a); - assertThat(filter.requiresAuthentication(request, new MockHttpServletResponse())).isTrue(); - Authentication result = filter.attemptAuthentication(request, new MockHttpServletResponse()); - assertThat(result != null).isTrue(); - } - - @Test - public void testNullServiceTicketHandledGracefully() throws Exception { - CasAuthenticationFilter filter = new CasAuthenticationFilter(); - filter.setAuthenticationManager((a) -> { - throw new BadCredentialsException("Rejected"); - }); - assertThatExceptionOfType(AuthenticationException.class).isThrownBy( - () -> filter.attemptAuthentication(new MockHttpServletRequest(), new MockHttpServletResponse())); - } - - @Test - public void testRequiresAuthenticationFilterProcessUrl() { - String url = "/login/cas"; - CasAuthenticationFilter filter = new CasAuthenticationFilter(); - filter.setFilterProcessesUrl(url); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - request.setServletPath(url); - assertThat(filter.requiresAuthentication(request, response)).isTrue(); - } - - @Test - public void testRequiresAuthenticationProxyRequest() { - CasAuthenticationFilter filter = new CasAuthenticationFilter(); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - request.setServletPath("/pgtCallback"); - assertThat(filter.requiresAuthentication(request, response)).isFalse(); - filter.setProxyReceptorUrl(request.getServletPath()); - assertThat(filter.requiresAuthentication(request, response)).isFalse(); - filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class)); - assertThat(filter.requiresAuthentication(request, response)).isTrue(); - request.setServletPath("/other"); - assertThat(filter.requiresAuthentication(request, response)).isFalse(); - } - - @Test - public void testRequiresAuthenticationAuthAll() { - ServiceProperties properties = new ServiceProperties(); - properties.setAuthenticateAllArtifacts(true); - String url = "/login/cas"; - CasAuthenticationFilter filter = new CasAuthenticationFilter(); - filter.setFilterProcessesUrl(url); - filter.setServiceProperties(properties); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - request.setServletPath(url); - assertThat(filter.requiresAuthentication(request, response)).isTrue(); - request.setServletPath("/other"); - assertThat(filter.requiresAuthentication(request, response)).isFalse(); - request.setParameter(properties.getArtifactParameter(), "value"); - assertThat(filter.requiresAuthentication(request, response)).isTrue(); - SecurityContextHolder.getContext() - .setAuthentication(new AnonymousAuthenticationToken("key", "principal", - AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"))); - assertThat(filter.requiresAuthentication(request, response)).isTrue(); - SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("un", "principal")); - assertThat(filter.requiresAuthentication(request, response)).isTrue(); - SecurityContextHolder.getContext() - .setAuthentication(new TestingAuthenticationToken("un", "principal", "ROLE_ANONYMOUS")); - assertThat(filter.requiresAuthentication(request, response)).isFalse(); - } - - @Test - public void testAuthenticateProxyUrl() throws Exception { - CasAuthenticationFilter filter = new CasAuthenticationFilter(); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - request.setServletPath("/pgtCallback"); - filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class)); - filter.setProxyReceptorUrl(request.getServletPath()); - assertThat(filter.attemptAuthentication(request, response)).isNull(); - } - - @Test - public void testDoFilterAuthenticateAll() throws Exception { - AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class); - AuthenticationManager manager = mock(AuthenticationManager.class); - Authentication authentication = new TestingAuthenticationToken("un", "pwd", "ROLE_USER"); - given(manager.authenticate(any(Authentication.class))).willReturn(authentication); - ServiceProperties serviceProperties = new ServiceProperties(); - serviceProperties.setAuthenticateAllArtifacts(true); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setParameter("ticket", "ST-1-123"); - request.setServletPath("/authenticate"); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain chain = mock(FilterChain.class); - CasAuthenticationFilter filter = new CasAuthenticationFilter(); - filter.setServiceProperties(serviceProperties); - filter.setAuthenticationSuccessHandler(successHandler); - filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class)); - filter.setAuthenticationManager(manager); - filter.afterPropertiesSet(); - filter.doFilter(request, response, chain); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull() - .withFailMessage("Authentication should not be null"); - verify(chain).doFilter(request, response); - verifyNoInteractions(successHandler); - // validate for when the filterProcessUrl matches - filter.setFilterProcessesUrl(request.getServletPath()); - SecurityContextHolder.clearContext(); - filter.doFilter(request, response, chain); - verifyNoMoreInteractions(chain); - verify(successHandler).onAuthenticationSuccess(request, response, authentication); - } - - // SEC-1592 - @Test - public void testChainNotInvokedForProxyReceptor() throws Exception { - CasAuthenticationFilter filter = new CasAuthenticationFilter(); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain chain = mock(FilterChain.class); - request.setServletPath("/pgtCallback"); - filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class)); - filter.setProxyReceptorUrl(request.getServletPath()); - filter.doFilter(request, response, chain); - verifyNoInteractions(chain); - } - - @Test - public void successfulAuthenticationWhenProxyRequestThenSavesSecurityContext() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setParameter(ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER, "ticket"); - - MockHttpServletResponse response = new MockHttpServletResponse(); - CasAuthenticationFilter filter = new CasAuthenticationFilter(); - ServiceProperties serviceProperties = new ServiceProperties(); - serviceProperties.setAuthenticateAllArtifacts(true); - filter.setServiceProperties(serviceProperties); - - SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class); - ReflectionTestUtils.setField(filter, "securityContextRepository", securityContextRepository); - - filter.successfulAuthentication(request, response, new MockFilterChain(), mock(Authentication.class)); - verify(securityContextRepository).saveContext(any(SecurityContext.class), eq(request), eq(response)); - } - -} diff --git a/cas/src/test/java/org/springframework/security/cas/web/ServicePropertiesTests.java b/cas/src/test/java/org/springframework/security/cas/web/ServicePropertiesTests.java deleted file mode 100644 index fc52e1d6a5..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/web/ServicePropertiesTests.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited - * - * 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.cas.web; - -import org.junit.jupiter.api.Test; - -import org.springframework.security.cas.SamlServiceProperties; -import org.springframework.security.cas.ServiceProperties; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * Tests {@link ServiceProperties}. - * - * @author Ben Alex - */ -public class ServicePropertiesTests { - - @Test - public void detectsMissingService() throws Exception { - ServiceProperties sp = new ServiceProperties(); - assertThatIllegalArgumentException().isThrownBy(sp::afterPropertiesSet); - } - - @Test - public void nullServiceWhenAuthenticateAllTokens() throws Exception { - ServiceProperties sp = new ServiceProperties(); - sp.setAuthenticateAllArtifacts(true); - assertThatIllegalArgumentException().isThrownBy(sp::afterPropertiesSet); - sp.setAuthenticateAllArtifacts(false); - assertThatIllegalArgumentException().isThrownBy(sp::afterPropertiesSet); - } - - @Test - public void testGettersSetters() throws Exception { - ServiceProperties[] sps = { new ServiceProperties(), new SamlServiceProperties() }; - for (ServiceProperties sp : sps) { - sp.setSendRenew(false); - assertThat(sp.isSendRenew()).isFalse(); - sp.setSendRenew(true); - assertThat(sp.isSendRenew()).isTrue(); - sp.setArtifactParameter("notticket"); - assertThat(sp.getArtifactParameter()).isEqualTo("notticket"); - sp.setServiceParameter("notservice"); - assertThat(sp.getServiceParameter()).isEqualTo("notservice"); - sp.setService("https://mycompany.com/service"); - assertThat(sp.getService()).isEqualTo("https://mycompany.com/service"); - sp.afterPropertiesSet(); - } - } - -} diff --git a/cas/src/test/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetailsTests.java b/cas/src/test/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetailsTests.java deleted file mode 100644 index f9553ce6eb..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/web/authentication/DefaultServiceAuthenticationDetailsTests.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2011-2023 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.cas.web.authentication; - -import java.util.regex.Pattern; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.support.GenericXmlApplicationContext; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.security.cas.ServiceProperties; -import org.springframework.security.web.util.UrlUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Rob Winch - */ -public class DefaultServiceAuthenticationDetailsTests { - - private DefaultServiceAuthenticationDetails details; - - private MockHttpServletRequest request; - - private Pattern artifactPattern; - - private String casServiceUrl; - - private ConfigurableApplicationContext context; - - @BeforeEach - public void setUp() { - this.casServiceUrl = "https://localhost:8443/j_spring_security_cas"; - this.request = new MockHttpServletRequest(); - this.request.setScheme("https"); - this.request.setServerName("localhost"); - this.request.setServerPort(8443); - this.request.setRequestURI("/cas-sample/secure/"); - this.artifactPattern = DefaultServiceAuthenticationDetails - .createArtifactPattern(ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER); - } - - @AfterEach - public void cleanup() { - if (this.context != null) { - this.context.close(); - } - } - - @Test - public void getServiceUrlNullQuery() throws Exception { - this.details = new DefaultServiceAuthenticationDetails(this.casServiceUrl, this.request, this.artifactPattern); - assertThat(this.details.getServiceUrl()).isEqualTo(UrlUtils.buildFullRequestUrl(this.request)); - } - - @Test - public void getServiceUrlTicketOnlyParam() throws Exception { - this.request.setQueryString("ticket=123"); - this.details = new DefaultServiceAuthenticationDetails(this.casServiceUrl, this.request, this.artifactPattern); - String serviceUrl = this.details.getServiceUrl(); - this.request.setQueryString(null); - assertThat(serviceUrl).isEqualTo(UrlUtils.buildFullRequestUrl(this.request)); - } - - @Test - public void getServiceUrlTicketFirstMultiParam() throws Exception { - this.request.setQueryString("ticket=123&other=value"); - this.details = new DefaultServiceAuthenticationDetails(this.casServiceUrl, this.request, this.artifactPattern); - String serviceUrl = this.details.getServiceUrl(); - this.request.setQueryString("other=value"); - assertThat(serviceUrl).isEqualTo(UrlUtils.buildFullRequestUrl(this.request)); - } - - @Test - public void getServiceUrlTicketLastMultiParam() throws Exception { - this.request.setQueryString("other=value&ticket=123"); - this.details = new DefaultServiceAuthenticationDetails(this.casServiceUrl, this.request, this.artifactPattern); - String serviceUrl = this.details.getServiceUrl(); - this.request.setQueryString("other=value"); - assertThat(serviceUrl).isEqualTo(UrlUtils.buildFullRequestUrl(this.request)); - } - - @Test - public void getServiceUrlTicketMiddleMultiParam() throws Exception { - this.request.setQueryString("other=value&ticket=123&last=this"); - this.details = new DefaultServiceAuthenticationDetails(this.casServiceUrl, this.request, this.artifactPattern); - String serviceUrl = this.details.getServiceUrl(); - this.request.setQueryString("other=value&last=this"); - assertThat(serviceUrl).isEqualTo(UrlUtils.buildFullRequestUrl(this.request)); - } - - @Test - public void getServiceUrlDoesNotUseHostHeader() throws Exception { - this.casServiceUrl = "https://example.com/j_spring_security_cas"; - this.request.setServerName("evil.com"); - this.details = new DefaultServiceAuthenticationDetails(this.casServiceUrl, this.request, this.artifactPattern); - assertThat(this.details.getServiceUrl()).isEqualTo("https://example.com/cas-sample/secure/"); - } - - @Test - public void getServiceUrlDoesNotUseHostHeaderExplicit() { - this.casServiceUrl = "https://example.com/j_spring_security_cas"; - this.request.setServerName("evil.com"); - ServiceAuthenticationDetails details = loadServiceAuthenticationDetails( - "defaultserviceauthenticationdetails-explicit.xml"); - assertThat(details.getServiceUrl()).isEqualTo("https://example.com/cas-sample/secure/"); - } - - private ServiceAuthenticationDetails loadServiceAuthenticationDetails(String resourceName) { - this.context = new GenericXmlApplicationContext(getClass(), resourceName); - ServiceAuthenticationDetailsSource source = this.context.getBean(ServiceAuthenticationDetailsSource.class); - return source.buildDetails(this.request); - } - -} diff --git a/cas/src/test/resources/logback-test.xml b/cas/src/test/resources/logback-test.xml deleted file mode 100644 index 2d51ba4180..0000000000 --- a/cas/src/test/resources/logback-test.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - - diff --git a/cas/src/test/resources/org/springframework/security/cas/web/authentication/defaultserviceauthenticationdetails-explicit.xml b/cas/src/test/resources/org/springframework/security/cas/web/authentication/defaultserviceauthenticationdetails-explicit.xml deleted file mode 100644 index c7d5346179..0000000000 --- a/cas/src/test/resources/org/springframework/security/cas/web/authentication/defaultserviceauthenticationdetails-explicit.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/cas/src/test/resources/org/springframework/security/cas/web/authentication/defaultserviceauthenticationdetails-passivity.xml b/cas/src/test/resources/org/springframework/security/cas/web/authentication/defaultserviceauthenticationdetails-passivity.xml deleted file mode 100644 index 0fe950ff2b..0000000000 --- a/cas/src/test/resources/org/springframework/security/cas/web/authentication/defaultserviceauthenticationdetails-passivity.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 05dbc07057..9aceabf3e3 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -39,7 +39,6 @@ dependencies { provided 'jakarta.servlet:jakarta.servlet-api' testImplementation project(':spring-security-aspects') - testImplementation project(':spring-security-cas') testImplementation project(':spring-security-test') testImplementation project(path : ':spring-security-core', configuration : 'tests') testImplementation project(path : ':spring-security-ldap', configuration : 'tests') @@ -80,6 +79,7 @@ dependencies { testImplementation "org.hibernate.orm:hibernate-core" testImplementation 'org.hsqldb:hsqldb' testImplementation 'org.mockito:mockito-core' + testImplementation "org.mockito:mockito-inline" testImplementation('org.seleniumhq.selenium:htmlunit-driver') { exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'xml-apis', module: 'xml-apis' diff --git a/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java b/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java index ef65fb55c7..2c30dce0f1 100644 --- a/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java +++ b/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java @@ -294,7 +294,7 @@ public class RSocketMessageHandlerITests { @MessageMapping({ "secure.send", "send" }) Mono send(Mono payload) { - return payload.doOnNext(this::add).then(Mono.fromRunnable(this::doNotifyAll)); + return payload.doOnNext(this::add).then(Mono.fromRunnable(() -> doNotifyAll())); } private synchronized void doNotifyAll() { diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapUserServiceBeanDefinitionParserTests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapUserServiceBeanDefinitionParserTests.java index cc195ca2fd..d43bc69d6b 100644 --- a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapUserServiceBeanDefinitionParserTests.java +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapUserServiceBeanDefinitionParserTests.java @@ -87,7 +87,7 @@ public class LdapUserServiceBeanDefinitionParserTests { Set authorities = AuthorityUtils.authorityListToSet(ben.getAuthorities()); assertThat(authorities).hasSize(3); - assertThat(authorities).contains("ROLE_DEVELOPERS"); + assertThat(authorities.contains("ROLE_DEVELOPERS")).isTrue(); } @Test @@ -128,7 +128,7 @@ public class LdapUserServiceBeanDefinitionParserTests { Set authorities = AuthorityUtils.authorityListToSet(ben.getAuthorities()); assertThat(authorities).hasSize(3); - assertThat(authorities).contains("ROLE_DEVELOPER"); + assertThat(authorities.contains("ROLE_DEVELOPER")).isTrue(); } diff --git a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java index a1921249fb..030c0bd379 100644 --- a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java +++ b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java @@ -96,7 +96,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler { pc.getReaderContext() .fatal("You cannot use a spring-security-2.0.xsd or spring-security-3.0.xsd or " + "spring-security-3.1.xsd schema or spring-security-3.2.xsd schema or spring-security-4.0.xsd schema " - + "with Spring Security 6.2. Please update your schema declarations to the 6.2 schema.", + + "with Spring Security 6.0. Please update your schema declarations to the 6.0 schema.", element); } String name = pc.getDelegate().getLocalName(element); @@ -221,7 +221,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler { private boolean matchesVersionInternal(Element element) { String schemaLocation = element.getAttributeNS("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation"); - return schemaLocation.matches("(?m).*spring-security-6\\.2.*.xsd.*") + return schemaLocation.matches("(?m).*spring-security-6\\.0.*.xsd.*") || schemaLocation.matches("(?m).*spring-security.xsd.*") || !schemaLocation.matches("(?m).*spring-security.*"); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java index 4293926460..d47cbd8491 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.util.Assert; import org.springframework.web.filter.DelegatingFilterProxy; @@ -118,10 +117,7 @@ public abstract class AbstractConfiguredSecurityBuilder> C apply(C configurer) throws Exception { configurer.addObjectPostProcessor(this.objectPostProcessor); @@ -143,23 +139,6 @@ public abstract class AbstractConfiguredSecurityBuilder> B with(C configurer, Customizer customizer) throws Exception { - configurer.addObjectPostProcessor(this.objectPostProcessor); - configurer.setBuilder((B) this); - add(configurer); - customizer.customize(configurer); - return (B) this; - } - /** * Sets an object that is shared by multiple {@link SecurityConfigurer}. * @param sharedType the Class to key the shared object by. diff --git a/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java index 7703c974bd..fd25c16d12 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/SecurityConfigurerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2013 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. @@ -53,9 +53,7 @@ public abstract class SecurityConfigurerAdapter> * Return the {@link SecurityBuilder} when done using the {@link SecurityConfigurer}. * This is useful for method chaining. * @return the {@link SecurityBuilder} for further customizations - * @deprecated For removal in 7.0. Use the lambda based configuration instead. */ - @Deprecated(since = "6.1", forRemoval = true) public B and() { return getBuilder(); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java index 4b561360a7..9a0d08a902 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java @@ -41,8 +41,7 @@ final class MethodSecuritySelector implements ImportSelector { @Override public String[] selectImports(@NonNull AnnotationMetadata importMetadata) { - if (!importMetadata.hasAnnotation(EnableMethodSecurity.class.getName()) - && !importMetadata.hasMetaAnnotation(EnableMethodSecurity.class.getName())) { + if (!importMetadata.hasAnnotation(EnableMethodSecurity.class.getName())) { return new String[0]; } EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java index 72dbee5365..c4047dd533 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ import java.util.Map; import jakarta.servlet.DispatcherType; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletRegistration; -import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; @@ -204,30 +203,11 @@ public abstract class AbstractRequestMatcherRegistry { if (!hasDispatcherServlet(registrations)) { return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns)); } - ServletRegistration dispatcherServlet = requireOneRootDispatcherServlet(registrations); - if (dispatcherServlet != null) { - if (registrations.size() == 1) { - return requestMatchers(createMvcMatchers(method, patterns).toArray(RequestMatcher[]::new)); - } - List matchers = new ArrayList<>(); - for (String pattern : patterns) { - AntPathRequestMatcher ant = new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null); - MvcRequestMatcher mvc = createMvcMatchers(method, pattern).get(0); - matchers.add(new DispatcherServletDelegatingRequestMatcher(ant, mvc, servletContext)); - } - return requestMatchers(matchers.toArray(new RequestMatcher[0])); + if (registrations.size() > 1) { + String errorMessage = computeErrorMessage(registrations.values()); + throw new IllegalArgumentException(errorMessage); } - dispatcherServlet = requireOnlyPathMappedDispatcherServlet(registrations); - if (dispatcherServlet != null) { - String mapping = dispatcherServlet.getMappings().iterator().next(); - List matchers = createMvcMatchers(method, patterns); - for (MvcRequestMatcher matcher : matchers) { - matcher.setServletPath(mapping.substring(0, mapping.length() - 2)); - } - return requestMatchers(matchers.toArray(new RequestMatcher[0])); - } - String errorMessage = computeErrorMessage(registrations.values()); - throw new IllegalArgumentException(errorMessage); + return requestMatchers(createMvcMatchers(method, patterns).toArray(new RequestMatcher[0])); } private Map mappableServletRegistrations(ServletContext servletContext) { @@ -245,66 +225,22 @@ public abstract class AbstractRequestMatcherRegistry { if (registrations == null) { return false; } + Class dispatcherServlet = ClassUtils.resolveClassName("org.springframework.web.servlet.DispatcherServlet", + null); for (ServletRegistration registration : registrations.values()) { - if (isDispatcherServlet(registration)) { - return true; + try { + Class clazz = Class.forName(registration.getClassName()); + if (dispatcherServlet.isAssignableFrom(clazz)) { + return true; + } + } + catch (ClassNotFoundException ex) { + return false; } } return false; } - private ServletRegistration requireOneRootDispatcherServlet( - Map registrations) { - ServletRegistration rootDispatcherServlet = null; - for (ServletRegistration registration : registrations.values()) { - if (!isDispatcherServlet(registration)) { - continue; - } - if (registration.getMappings().size() > 1) { - return null; - } - if (!"/".equals(registration.getMappings().iterator().next())) { - return null; - } - rootDispatcherServlet = registration; - } - return rootDispatcherServlet; - } - - private ServletRegistration requireOnlyPathMappedDispatcherServlet( - Map registrations) { - ServletRegistration pathDispatcherServlet = null; - for (ServletRegistration registration : registrations.values()) { - if (!isDispatcherServlet(registration)) { - return null; - } - if (registration.getMappings().size() > 1) { - return null; - } - String mapping = registration.getMappings().iterator().next(); - if (!mapping.startsWith("/") || !mapping.endsWith("/*")) { - return null; - } - if (pathDispatcherServlet != null) { - return null; - } - pathDispatcherServlet = registration; - } - return pathDispatcherServlet; - } - - private boolean isDispatcherServlet(ServletRegistration registration) { - Class dispatcherServlet = ClassUtils.resolveClassName("org.springframework.web.servlet.DispatcherServlet", - null); - try { - Class clazz = Class.forName(registration.getClassName()); - return dispatcherServlet.isAssignableFrom(clazz); - } - catch (ClassNotFoundException ex) { - return false; - } - } - private String computeErrorMessage(Collection registrations) { String template = "This method cannot decide whether these patterns are Spring MVC patterns or not. " + "If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); " @@ -444,55 +380,4 @@ public abstract class AbstractRequestMatcherRegistry { } - static class DispatcherServletDelegatingRequestMatcher implements RequestMatcher { - - private final AntPathRequestMatcher ant; - - private final MvcRequestMatcher mvc; - - private final ServletContext servletContext; - - DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc, - ServletContext servletContext) { - this.ant = ant; - this.mvc = mvc; - this.servletContext = servletContext; - } - - @Override - public boolean matches(HttpServletRequest request) { - String name = request.getHttpServletMapping().getServletName(); - ServletRegistration registration = this.servletContext.getServletRegistration(name); - Assert.notNull(registration, "Failed to find servlet [" + name + "] in the servlet context"); - if (isDispatcherServlet(registration)) { - return this.mvc.matches(request); - } - return this.ant.matches(request); - } - - @Override - public MatchResult matcher(HttpServletRequest request) { - String name = request.getHttpServletMapping().getServletName(); - ServletRegistration registration = this.servletContext.getServletRegistration(name); - Assert.notNull(registration, "Failed to find servlet [" + name + "] in the servlet context"); - if (isDispatcherServlet(registration)) { - return this.mvc.matcher(request); - } - return this.ant.matcher(request); - } - - private boolean isDispatcherServlet(ServletRegistration registration) { - Class dispatcherServlet = ClassUtils - .resolveClassName("org.springframework.web.servlet.DispatcherServlet", null); - try { - Class clazz = Class.forName(registration.getClassName()); - return dispatcherServlet.isAssignableFrom(clazz); - } - catch (ClassNotFoundException ex) { - return false; - } - } - - } - } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java index 25d7f95287..cddb6ce5e4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -127,7 +127,11 @@ final class FilterOrderRegistration { * @param position the position to associate with the {@link Filter} */ void put(Class filter, int position) { - this.filterToOrder.putIfAbsent(filter.getName(), position); + String className = filter.getName(); + if (this.filterToOrder.containsKey(className)) { + return; + } + this.filterToOrder.put(className, position); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 9d0333d24e..81582f7631 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -70,11 +70,9 @@ import org.springframework.security.config.annotation.web.configurers.SessionMan import org.springframework.security.config.annotation.web.configurers.X509Configurer; import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer; -import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer; import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer; -import org.springframework.security.config.annotation.web.configurers.saml2.Saml2MetadataConfigurer; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -286,13 +284,8 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link HeadersConfigurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #headers(Customizer)} or - * {@code headers(Customizer.withDefaults())} to stick with defaults. See the documentation - * for more details. * @see HeadersConfigurer */ - @Deprecated(since = "6.1", forRemoval = true) public HeadersConfigurer headers() throws Exception { return getOrApply(new HeadersConfigurer<>()); } @@ -405,12 +398,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderdocumentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public CorsConfigurer cors() throws Exception { return getOrApply(new CorsConfigurer<>()); } @@ -497,13 +485,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderdocumentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public SessionManagementConfigurer sessionManagement() throws Exception { return getOrApply(new SessionManagementConfigurer<>()); } @@ -625,14 +607,8 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link PortMapperConfigurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #portMapper(Customizer)} or - * {@code portMapper(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. * @see #requiresChannel() */ - @Deprecated(since = "6.1", forRemoval = true) public PortMapperConfigurer portMapper() throws Exception { return getOrApply(new PortMapperConfigurer<>()); } @@ -762,12 +738,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderdocumentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public JeeConfigurer jee() throws Exception { return getOrApply(new JeeConfigurer<>()); } @@ -878,12 +849,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link X509Configurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #x509(Customizer)} or - * {@code x509(Customizer.withDefaults())} to stick with defaults. See the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public X509Configurer x509() throws Exception { return getOrApply(new X509Configurer<>()); } @@ -961,13 +927,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link RememberMeConfigurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #rememberMe(Customizer)} or - * {@code rememberMe(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public RememberMeConfigurer rememberMe() throws Exception { return getOrApply(new RememberMeConfigurer<>()); } @@ -1111,7 +1071,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #authorizeHttpRequests()} instead + * @deprecated Use {@link #authorizeHttpRequests()} instead */ @Deprecated public ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry authorizeRequests() @@ -1226,7 +1186,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder.AuthorizationManagerRequestMatcherRegistry authorizeHttpRequests() throws Exception { ApplicationContext context = getContext(); @@ -1476,13 +1433,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderdocumentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public RequestCacheConfigurer requestCache() throws Exception { return getOrApply(new RequestCacheConfigurer<>()); } @@ -1533,13 +1484,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderdocumentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ExceptionHandlingConfigurer exceptionHandling() throws Exception { return getOrApply(new ExceptionHandlingConfigurer<>()); } @@ -1591,13 +1536,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderdocumentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public SecurityContextConfigurer securityContext() throws Exception { return getOrApply(new SecurityContextConfigurer<>()); } @@ -1642,13 +1581,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderdocumentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ServletApiConfigurer servletApi() throws Exception { return getOrApply(new ServletApiConfigurer<>()); } @@ -1704,12 +1637,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link CsrfConfigurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #csrf(Customizer)} or - * {@code csrf(Customizer.withDefaults())} to stick with defaults. See the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public CsrfConfigurer csrf() throws Exception { ApplicationContext context = getContext(); return getOrApply(new CsrfConfigurer<>(context)); @@ -1784,12 +1712,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link LogoutConfigurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #logout(Customizer)} or - * {@code logout(Customizer.withDefaults())} to stick with defaults. See the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public LogoutConfigurer logout() throws Exception { return getOrApply(new LogoutConfigurer<>()); } @@ -1928,13 +1851,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link AnonymousConfigurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #anonymous(Customizer)} or - * {@code anonymous(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public AnonymousConfigurer anonymous() throws Exception { return getOrApply(new AnonymousConfigurer<>()); } @@ -2097,14 +2014,8 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link FormLoginConfigurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #formLogin(Customizer)} or - * {@code formLogin(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. * @see FormLoginConfigurer#loginPage(String) */ - @Deprecated(since = "6.1", forRemoval = true) public FormLoginConfigurer formLogin() throws Exception { return getOrApply(new FormLoginConfigurer<>()); } @@ -2279,13 +2190,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderdocumentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public Saml2LoginConfigurer saml2Login() throws Exception { return getOrApply(new Saml2LoginConfigurer<>()); } @@ -2515,119 +2420,11 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderdocumentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public Saml2LogoutConfigurer saml2Logout() throws Exception { return getOrApply(new Saml2LogoutConfigurer<>(getContext())); } - /** - * Configures a SAML 2.0 metadata endpoint that presents relying party configurations - * in an {@code } payload. - * - *

- * By default, the endpoints are {@code /saml2/metadata} and - * {@code /saml2/metadata/{registrationId}} though note that also - * {@code /saml2/service-provider-metadata/{registrationId}} is recognized for - * backward compatibility purposes. - * - *

- *

Example Configuration

- * - * The following example shows the minimal configuration required, using a - * hypothetical asserting party. - * - *
-	 *	@EnableWebSecurity
-	 *	@Configuration
-	 *	public class Saml2LogoutSecurityConfig {
-	 *		@Bean
-	 *		public SecurityFilterChain web(HttpSecurity http) throws Exception {
-	 *			http
-	 *				.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
-	 *				.saml2Metadata(Customizer.withDefaults());
-	 *			return http.build();
-	 *		}
-	 *
-	 *		@Bean
-	 *		public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
-	 *			RelyingPartyRegistration registration = RelyingPartyRegistrations
-	 *					.withMetadataLocation("https://ap.example.org/metadata")
-	 *					.registrationId("simple")
-	 *					.build();
-	 *			return new InMemoryRelyingPartyRegistrationRepository(registration);
-	 *		}
-	 *	}
-	 * 
- * @param saml2MetadataConfigurer the {@link Customizer} to provide more options for - * the {@link Saml2MetadataConfigurer} - * @return the {@link HttpSecurity} for further customizations - * @throws Exception - * @since 6.1 - */ - public HttpSecurity saml2Metadata(Customizer> saml2MetadataConfigurer) - throws Exception { - saml2MetadataConfigurer.customize(getOrApply(new Saml2MetadataConfigurer<>(getContext()))); - return HttpSecurity.this; - } - - /** - * Configures a SAML 2.0 metadata endpoint that presents relying party configurations - * in an {@code } payload. - * - *

- * By default, the endpoints are {@code /saml2/metadata} and - * {@code /saml2/metadata/{registrationId}} though note that also - * {@code /saml2/service-provider-metadata/{registrationId}} is recognized for - * backward compatibility purposes. - * - *

- *

Example Configuration

- * - * The following example shows the minimal configuration required, using a - * hypothetical asserting party. - * - *
-	 *	@EnableWebSecurity
-	 *	@Configuration
-	 *	public class Saml2LogoutSecurityConfig {
-	 *		@Bean
-	 *		public SecurityFilterChain web(HttpSecurity http) throws Exception {
-	 *			http
-	 *				.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
-	 *				.saml2Metadata(Customizer.withDefaults());
-	 *			return http.build();
-	 *		}
-	 *
-	 *		@Bean
-	 *		public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
-	 *			RelyingPartyRegistration registration = RelyingPartyRegistrations
-	 *					.withMetadataLocation("https://ap.example.org/metadata")
-	 *					.registrationId("simple")
-	 *					.build();
-	 *			return new InMemoryRelyingPartyRegistrationRepository(registration);
-	 *		}
-	 *	}
-	 * 
- * @return the {@link Saml2MetadataConfigurer} for further customizations - * @throws Exception - * @since 6.1 - * @deprecated For removal in 7.0. Use {@link #saml2Metadata(Customizer)} or - * {@code saml2Metadata(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. - */ - @Deprecated(since = "6.1", forRemoval = true) - public Saml2MetadataConfigurer saml2Metadata() throws Exception { - return getOrApply(new Saml2MetadataConfigurer<>(getContext())); - } - /** * Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 * Provider.
@@ -2714,11 +2511,6 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderdocumentation - * for more details. * @see Section 4.1 Authorization Code * Grant @@ -2728,7 +2520,6 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder oauth2Login() throws Exception { return getOrApply(new OAuth2LoginConfigurer<>()); } @@ -2836,31 +2627,15 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder oidcLogout() throws Exception { - return getOrApply(new OidcLogoutConfigurer<>()); - } - - public HttpSecurity oidcLogout(Customizer> oidcLogoutCustomizer) - throws Exception { - oidcLogoutCustomizer.customize(getOrApply(new OidcLogoutConfigurer<>())); - return HttpSecurity.this; - } - /** * Configures OAuth 2.0 Client support. * @return the {@link OAuth2ClientConfigurer} for further customizations * @throws Exception * @since 5.1 - * @deprecated For removal in 7.0. Use {@link #oauth2Client(Customizer)} or - * {@code oauth2Client(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. * @see OAuth 2.0 Authorization * Framework */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2ClientConfigurer oauth2Client() throws Exception { OAuth2ClientConfigurer configurer = getOrApply(new OAuth2ClientConfigurer<>()); this.postProcess(configurer); @@ -2911,13 +2686,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilderOAuth 2.0 Authorization * Framework */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2ResourceServerConfigurer oauth2ResourceServer() throws Exception { OAuth2ResourceServerConfigurer configurer = getOrApply( new OAuth2ResourceServerConfigurer<>(getContext())); @@ -3015,13 +2787,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link ChannelSecurityConfigurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #requiresChannel(Customizer)} or - * {@code requiresChannel(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ChannelSecurityConfigurer.ChannelRequestMatcherRegistry requiresChannel() throws Exception { ApplicationContext context = getContext(); return getOrApply(new ChannelSecurityConfigurer<>(context)).getRegistry(); @@ -3116,13 +2882,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link HttpBasicConfigurer} for further customizations * @throws Exception - * @deprecated For removal in 7.0. Use {@link #httpBasic(Customizer)} or - * {@code httpBasic(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public HttpBasicConfigurer httpBasic() throws Exception { return getOrApply(new HttpBasicConfigurer<>()); } @@ -3451,13 +3211,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @return the {@link RequestMatcherConfigurer} for further customizations - * @deprecated For removal in 7.0. Use {@link #securityMatchers(Customizer)} or - * {@code securityMatchers(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public RequestMatcherConfigurer securityMatchers() { return this.requestMatcherConfigurer; } @@ -3715,28 +3469,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder - * @Configuration - * @EnableWebSecurity - * public class SecurityConfig { - * - * @Bean - * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - * http - * .securityMatchers((matchers) -> matchers - * .requestMatchers("/api/**") - * ) - * .authorizeHttpRequests((authorize) -> authorize - * .anyRequest().hasRole("USER") - * ) - * .httpBasic(Customizer.withDefaults()); - * return http.build(); - * } - * - * } - * */ - @Deprecated(since = "6.1", forRemoval = true) public HttpSecurity and() { return HttpSecurity.this; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java index d00242377e..bac12ece45 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java @@ -46,7 +46,7 @@ import org.springframework.security.web.SecurityFilterChain; * * @Bean * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - * http.authorizeHttpRequests().requestMatchers("/public/**").permitAll().anyRequest() + * http.authorizeRequests().requestMatchers("/public/**").permitAll().anyRequest() * .hasRole("USER").and() * // Possibly more configuration ... * .formLogin() // enable form based log in diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index ba962a4fad..3889aa6d11 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,6 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.web.accept.ContentNegotiationStrategy; import org.springframework.web.accept.HeaderContentNegotiationStrategy; -import org.springframework.web.cors.CorsConfigurationSource; import static org.springframework.security.config.Customizer.withDefaults; @@ -125,18 +124,10 @@ class HttpSecurityConfiguration { .apply(new DefaultLoginPageConfigurer<>()); http.logout(withDefaults()); // @formatter:on - applyCorsIfAvailable(http); applyDefaultConfigurers(http); return http; } - private void applyCorsIfAvailable(HttpSecurity http) throws Exception { - String[] beanNames = this.context.getBeanNamesForType(CorsConfigurationSource.class); - if (beanNames.length == 1) { - http.cors(withDefaults()); - } - } - private AuthenticationManager authenticationManager() throws Exception { return this.authenticationConfiguration.getAuthenticationManager(); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java index 668963f9d5..0fc647de67 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,46 +16,19 @@ package org.springframework.security.config.annotation.web.configuration; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.Set; -import java.util.function.Consumer; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.BeanInitializationException; -import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; -import org.springframework.context.annotation.AnnotationBeanNameGenerator; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; -import org.springframework.core.ResolvableType; import org.springframework.core.type.AnnotationMetadata; import org.springframework.security.core.context.SecurityContextHolderStrategy; -import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; @@ -75,8 +48,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; * @since 5.1 * @see OAuth2ImportSelector */ -@Import({ OAuth2ClientConfiguration.OAuth2ClientWebMvcImportSelector.class, - OAuth2ClientConfiguration.OAuth2AuthorizedClientManagerConfiguration.class }) +@Import(OAuth2ClientConfiguration.OAuth2ClientWebMvcImportSelector.class) final class OAuth2ClientConfiguration { private static final boolean webMvcPresent; @@ -93,22 +65,8 @@ final class OAuth2ClientConfiguration { if (!webMvcPresent) { return new String[0]; } - return new String[] { - OAuth2ClientConfiguration.class.getName() + ".OAuth2ClientWebMvcSecurityConfiguration" }; - } - - } - - /** - * @author Joe Grandja - * @since 6.2.0 - */ - @Configuration(proxyBeanMethods = false) - static class OAuth2AuthorizedClientManagerConfiguration { - - @Bean - OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar() { - return new OAuth2AuthorizedClientManagerRegistrar(); + return new String[] { "org.springframework.security.config.annotation.web.configuration." + + "OAuth2ClientConfiguration.OAuth2ClientWebMvcSecurityConfiguration" }; } } @@ -116,12 +74,16 @@ final class OAuth2ClientConfiguration { @Configuration(proxyBeanMethods = false) static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer { + private ClientRegistrationRepository clientRegistrationRepository; + + private OAuth2AuthorizedClientRepository authorizedClientRepository; + + private OAuth2AccessTokenResponseClient accessTokenResponseClient; + private OAuth2AuthorizedClientManager authorizedClientManager; private SecurityContextHolderStrategy securityContextHolderStrategy; - private OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar; - @Override public void addArgumentResolvers(List argumentResolvers) { OAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager(); @@ -135,6 +97,26 @@ final class OAuth2ClientConfiguration { } } + @Autowired(required = false) + void setClientRegistrationRepository(List clientRegistrationRepositories) { + if (clientRegistrationRepositories.size() == 1) { + this.clientRegistrationRepository = clientRegistrationRepositories.get(0); + } + } + + @Autowired(required = false) + void setAuthorizedClientRepository(List authorizedClientRepositories) { + if (authorizedClientRepositories.size() == 1) { + this.authorizedClientRepository = authorizedClientRepositories.get(0); + } + } + + @Autowired(required = false) + void setAccessTokenResponseClient( + OAuth2AccessTokenResponseClient accessTokenResponseClient) { + this.accessTokenResponseClient = accessTokenResponseClient; + } + @Autowired(required = false) void setAuthorizedClientManager(List authorizedClientManagers) { if (authorizedClientManagers.size() == 1) { @@ -147,262 +129,35 @@ final class OAuth2ClientConfiguration { this.securityContextHolderStrategy = strategy; } - @Autowired - void setAuthorizedClientManagerRegistrar( - OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) { - this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar; - } - private OAuth2AuthorizedClientManager getAuthorizedClientManager() { if (this.authorizedClientManager != null) { return this.authorizedClientManager; } - return this.authorizedClientManagerRegistrar.getAuthorizedClientManagerIfAvailable(); - } - - } - - /** - * A registrar for registering the default {@link OAuth2AuthorizedClientManager} bean - * definition, if not already present. - * - * @author Joe Grandja - * @author Steve Riesenberg - * @since 6.2.0 - */ - static final class OAuth2AuthorizedClientManagerRegistrar - implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware { - - // @formatter:off - private static final Set> KNOWN_AUTHORIZED_CLIENT_PROVIDERS = Set.of( - AuthorizationCodeOAuth2AuthorizedClientProvider.class, - RefreshTokenOAuth2AuthorizedClientProvider.class, - ClientCredentialsOAuth2AuthorizedClientProvider.class, - PasswordOAuth2AuthorizedClientProvider.class, - JwtBearerOAuth2AuthorizedClientProvider.class - ); - // @formatter:on - - private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator(); - - private ListableBeanFactory beanFactory; - - @Override - public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { - if (getBeanNamesForType(OAuth2AuthorizedClientManager.class).length != 0 - || getBeanNamesForType(ClientRegistrationRepository.class).length != 1 - || getBeanNamesForType(OAuth2AuthorizedClientRepository.class).length != 1) { - return; - } - - BeanDefinition beanDefinition = BeanDefinitionBuilder - .genericBeanDefinition(OAuth2AuthorizedClientManager.class, this::getAuthorizedClientManager) - .getBeanDefinition(); - - registry.registerBeanDefinition(this.beanNameGenerator.generateBeanName(beanDefinition, registry), - beanDefinition); - } - - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = (ListableBeanFactory) beanFactory; - } - - OAuth2AuthorizedClientManager getAuthorizedClientManagerIfAvailable() { - if (getBeanNamesForType(ClientRegistrationRepository.class).length != 1 - || getBeanNamesForType(OAuth2AuthorizedClientRepository.class).length != 1) { - return null; - } - return getAuthorizedClientManager(); - } - - private OAuth2AuthorizedClientManager getAuthorizedClientManager() { - ClientRegistrationRepository clientRegistrationRepository = BeanFactoryUtils - .beanOfTypeIncludingAncestors(this.beanFactory, ClientRegistrationRepository.class, true, true); - - OAuth2AuthorizedClientRepository authorizedClientRepository = BeanFactoryUtils - .beanOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientRepository.class, true, true); - - Collection authorizedClientProviderBeans = BeanFactoryUtils - .beansOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientProvider.class, true, true) - .values(); - - OAuth2AuthorizedClientProvider authorizedClientProvider; - if (hasDelegatingAuthorizedClientProvider(authorizedClientProviderBeans)) { - authorizedClientProvider = authorizedClientProviderBeans.iterator().next(); - } - else { - List authorizedClientProviders = new ArrayList<>(); - authorizedClientProviders - .add(getAuthorizationCodeAuthorizedClientProvider(authorizedClientProviderBeans)); - authorizedClientProviders.add(getRefreshTokenAuthorizedClientProvider(authorizedClientProviderBeans)); - authorizedClientProviders - .add(getClientCredentialsAuthorizedClientProvider(authorizedClientProviderBeans)); - authorizedClientProviders.add(getPasswordAuthorizedClientProvider(authorizedClientProviderBeans)); - - OAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = getJwtBearerAuthorizedClientProvider( - authorizedClientProviderBeans); - if (jwtBearerAuthorizedClientProvider != null) { - authorizedClientProviders.add(jwtBearerAuthorizedClientProvider); + OAuth2AuthorizedClientManager authorizedClientManager = null; + if (this.clientRegistrationRepository != null && this.authorizedClientRepository != null) { + if (this.accessTokenResponseClient != null) { + // @formatter:off + OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder + .builder() + .authorizationCode() + .refreshToken() + .clientCredentials((configurer) -> configurer.accessTokenResponseClient(this.accessTokenResponseClient)) + .password() + .build(); + // @formatter:on + DefaultOAuth2AuthorizedClientManager defaultAuthorizedClientManager = new DefaultOAuth2AuthorizedClientManager( + this.clientRegistrationRepository, this.authorizedClientRepository); + defaultAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + authorizedClientManager = defaultAuthorizedClientManager; + } + else { + authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( + this.clientRegistrationRepository, this.authorizedClientRepository); } - - authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans)); - authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders); } - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - Consumer authorizedClientManagerConsumer = getBeanOfType( - ResolvableType.forClassWithGenerics(Consumer.class, DefaultOAuth2AuthorizedClientManager.class)); - if (authorizedClientManagerConsumer != null) { - authorizedClientManagerConsumer.accept(authorizedClientManager); - } - return authorizedClientManager; } - private boolean hasDelegatingAuthorizedClientProvider( - Collection authorizedClientProviders) { - if (authorizedClientProviders.size() != 1) { - return false; - } - return authorizedClientProviders.iterator().next() instanceof DelegatingOAuth2AuthorizedClientProvider; - } - - private OAuth2AuthorizedClientProvider getAuthorizationCodeAuthorizedClientProvider( - Collection authorizedClientProviders) { - AuthorizationCodeOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( - authorizedClientProviders, AuthorizationCodeOAuth2AuthorizedClientProvider.class); - if (authorizedClientProvider == null) { - authorizedClientProvider = new AuthorizationCodeOAuth2AuthorizedClientProvider(); - } - - return authorizedClientProvider; - } - - private OAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider( - Collection authorizedClientProviders) { - RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( - authorizedClientProviders, RefreshTokenOAuth2AuthorizedClientProvider.class); - if (authorizedClientProvider == null) { - authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider(); - } - - OAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType( - ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class, - OAuth2RefreshTokenGrantRequest.class)); - if (accessTokenResponseClient != null) { - authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); - } - - return authorizedClientProvider; - } - - private OAuth2AuthorizedClientProvider getClientCredentialsAuthorizedClientProvider( - Collection authorizedClientProviders) { - ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( - authorizedClientProviders, ClientCredentialsOAuth2AuthorizedClientProvider.class); - if (authorizedClientProvider == null) { - authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider(); - } - - OAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType( - ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class, - OAuth2ClientCredentialsGrantRequest.class)); - if (accessTokenResponseClient != null) { - authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); - } - - return authorizedClientProvider; - } - - private OAuth2AuthorizedClientProvider getPasswordAuthorizedClientProvider( - Collection authorizedClientProviders) { - PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( - authorizedClientProviders, PasswordOAuth2AuthorizedClientProvider.class); - if (authorizedClientProvider == null) { - authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider(); - } - - OAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType( - ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class, - OAuth2PasswordGrantRequest.class)); - if (accessTokenResponseClient != null) { - authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); - } - - return authorizedClientProvider; - } - - private OAuth2AuthorizedClientProvider getJwtBearerAuthorizedClientProvider( - Collection authorizedClientProviders) { - JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( - authorizedClientProviders, JwtBearerOAuth2AuthorizedClientProvider.class); - - OAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType( - ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class, - JwtBearerGrantRequest.class)); - if (accessTokenResponseClient != null) { - if (authorizedClientProvider == null) { - authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider(); - } - - authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); - } - - return authorizedClientProvider; - } - - private List getAdditionalAuthorizedClientProviders( - Collection authorizedClientProviders) { - List additionalAuthorizedClientProviders = new ArrayList<>( - authorizedClientProviders); - additionalAuthorizedClientProviders - .removeIf((provider) -> KNOWN_AUTHORIZED_CLIENT_PROVIDERS.contains(provider.getClass())); - return additionalAuthorizedClientProviders; - } - - private T getAuthorizedClientProviderByType( - Collection authorizedClientProviders, Class providerClass) { - T authorizedClientProvider = null; - for (OAuth2AuthorizedClientProvider current : authorizedClientProviders) { - if (providerClass.isInstance(current)) { - assertAuthorizedClientProviderIsNull(authorizedClientProvider); - authorizedClientProvider = providerClass.cast(current); - } - } - return authorizedClientProvider; - } - - private static void assertAuthorizedClientProviderIsNull( - OAuth2AuthorizedClientProvider authorizedClientProvider) { - if (authorizedClientProvider != null) { - // @formatter:off - throw new BeanInitializationException(String.format( - "Unable to create an %s bean. Expected one bean of type %s, but found multiple. " + - "Please consider defining only a single bean of this type, or define an %s bean yourself.", - OAuth2AuthorizedClientManager.class.getName(), - authorizedClientProvider.getClass().getName(), - OAuth2AuthorizedClientManager.class.getName())); - // @formatter:on - } - } - - private String[] getBeanNamesForType(Class beanClass) { - return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, beanClass, true, true); - } - - private T getBeanOfType(ResolvableType resolvableType) { - ObjectProvider objectProvider = this.beanFactory.getBeanProvider(resolvableType, true); - return objectProvider.getIfAvailable(); - } - } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java index 6e33d02183..829aac8ed8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,8 +51,8 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy; * Base class for configuring {@link AbstractAuthenticationFilterConfigurer}. This is * intended for internal use only. * - * @param refers to "this" for returning the current configurer - * @param refers to the {@link AbstractAuthenticationProcessingFilter} that is being + * @param T refers to "this" for returning the current configurer + * @param F refers to the {@link AbstractAuthenticationProcessingFilter} that is being * built * @author Rob Winch * @since 3.2 @@ -122,7 +122,7 @@ public abstract class AbstractAuthenticationFilterConfigurer extends AbstractRequestMatcherRegistry { - - private final RequestMatcherBuilder builder; - - AbstractRequestMatcherBuilderRegistry(ApplicationContext context) { - this(context, RequestMatcherBuilders.createDefault(context)); - } - - AbstractRequestMatcherBuilderRegistry(ApplicationContext context, RequestMatcherBuilder builder) { - setApplicationContext(context); - this.builder = builder; - } - - @Override - public final C requestMatchers(String... patterns) { - return requestMatchers(null, patterns); - } - - @Override - public final C requestMatchers(HttpMethod method, String... patterns) { - return requestMatchers(this.builder.matchers(method, patterns).toArray(RequestMatcher[]::new)); - } - - @Override - public final C requestMatchers(HttpMethod method) { - return requestMatchers(method, "/**"); - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AntPathRequestMatcherBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AntPathRequestMatcherBuilder.java deleted file mode 100644 index 4026fa2d2f..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AntPathRequestMatcherBuilder.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers; - -import org.springframework.http.HttpMethod; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; - -final class AntPathRequestMatcherBuilder implements RequestMatcherBuilder { - - private final String servletPath; - - private AntPathRequestMatcherBuilder(String servletPath) { - this.servletPath = servletPath; - } - - static AntPathRequestMatcherBuilder absolute() { - return new AntPathRequestMatcherBuilder(null); - } - - static AntPathRequestMatcherBuilder relativeTo(String path) { - return new AntPathRequestMatcherBuilder(path); - } - - @Override - public AntPathRequestMatcher matcher(String pattern) { - return matcher((String) null, pattern); - } - - @Override - public AntPathRequestMatcher matcher(HttpMethod method, String pattern) { - return matcher((method != null) ? method.name() : null, pattern); - } - - private AntPathRequestMatcher matcher(String method, String pattern) { - return new AntPathRequestMatcher(prependServletPath(pattern), method); - } - - private String prependServletPath(String pattern) { - if (this.servletPath == null) { - return pattern; - } - return this.servletPath + pattern; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java index f67ba9ad4c..a94337e426 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,12 @@ package org.springframework.security.config.annotation.web.configurers; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.function.Supplier; import io.micrometer.observation.ObservationRegistry; -import jakarta.servlet.http.HttpServletMapping; import jakarta.servlet.http.HttpServletRequest; import org.springframework.context.ApplicationContext; -import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; -import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.authorization.AuthenticatedAuthorizationManager; import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; @@ -36,22 +29,15 @@ import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.SpringAuthorizationEventPublisher; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager; -import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.util.Assert; -import org.springframework.util.function.SingletonSupplier; -import org.springframework.web.servlet.DispatcherServlet; /** * Adds a URL based authorization using {@link AuthorizationManager}. @@ -70,10 +56,6 @@ public final class AuthorizeHttpRequestsConfigurer roleHierarchy; - - private String rolePrefix = "ROLE_"; - /** * Creates an instance. * @param context the {@link ApplicationContext} to use @@ -86,13 +68,6 @@ public final class AuthorizeHttpRequestsConfigurer (context.getBeanNamesForType(RoleHierarchy.class).length > 0) - ? context.getBean(RoleHierarchy.class) : new NullRoleHierarchy()); - String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class); - if (grantedAuthorityDefaultsBeanNames.length > 0) { - GrantedAuthorityDefaults grantedAuthorityDefaults = context.getBean(GrantedAuthorityDefaults.class); - this.rolePrefix = grantedAuthorityDefaults.getRolePrefix(); - } } /** @@ -146,62 +121,41 @@ public final class AuthorizeHttpRequestsConfigurer { + extends AbstractRequestMatcherRegistry { private final RequestMatcherDelegatingAuthorizationManager.Builder managerBuilder = RequestMatcherDelegatingAuthorizationManager .builder(); - List unmappedMatchers; + private List unmappedMatchers; private int mappingCount; private boolean shouldFilterAllDispatcherTypes = true; - private final Map servletPattern = new LinkedHashMap<>(); - - AuthorizationManagerRequestMatcherRegistry(ApplicationContext context) { - super(context); + private AuthorizationManagerRequestMatcherRegistry(ApplicationContext context) { + setApplicationContext(context); } private void addMapping(RequestMatcher matcher, AuthorizationManager manager) { - Assert.isTrue(this.servletPattern.isEmpty(), - "Since you have used forServletPattern, all request matchers must be configured using forServletPattern; alternatively, you can use requestMatchers(RequestMatcher) for all requests."); this.unmappedMatchers = null; this.managerBuilder.add(matcher, manager); this.mappingCount++; } private void addFirst(RequestMatcher matcher, AuthorizationManager manager) { - Assert.isTrue(this.servletPattern.isEmpty(), - "Since you have used forServletPattern, all request matchers must be configured using forServletPattern; alternatively, you can use requestMatchers(RequestMatcher) for all requests."); this.unmappedMatchers = null; this.managerBuilder.mappings((m) -> m.add(0, new RequestMatcherEntry<>(matcher, manager))); this.mappingCount++; } - private AuthorizationManager servletAuthorizationManager() { - for (Map.Entry entry : this.servletPattern - .entrySet()) { - AuthorizationManagerServletRequestMatcherRegistry registry = entry.getValue(); - this.managerBuilder.add(new ServletPatternRequestMatcher(entry.getKey()), - registry.authorizationManager()); - } - return postProcess(this.managerBuilder.build()); - } - - private AuthorizationManager authorizationManager() { + private AuthorizationManager createAuthorizationManager() { Assert.state(this.unmappedMatchers == null, () -> "An incomplete mapping was found for " + this.unmappedMatchers + ". Try completing it with something like requestUrls()..hasRole('USER')"); Assert.state(this.mappingCount > 0, "At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())"); - return postProcess(this.managerBuilder.build()); - } - - private AuthorizationManager createAuthorizationManager() { - AuthorizationManager manager = (this.servletPattern.isEmpty()) ? authorizationManager() - : servletAuthorizationManager(); ObservationRegistry registry = getObservationRegistry(); + RequestMatcherDelegatingAuthorizationManager manager = postProcess(this.managerBuilder.build()); if (registry.isNoop()) { return manager; } @@ -211,74 +165,7 @@ public final class AuthorizeHttpRequestsConfigurer requestMatchers) { this.unmappedMatchers = requestMatchers; - return new AuthorizedUrl( - (manager) -> AuthorizeHttpRequestsConfigurer.this.addMapping(requestMatchers, manager)); - } - - /** - * Begin registering {@link RequestMatcher}s based on the type of the servlet - * mapped to {@code pattern}. Each registered request matcher will additionally - * check {@link HttpServletMapping#getPattern} against the provided - * {@code pattern}. - * - *

- * If the corresponding servlet is of type {@link DispatcherServlet}, then use a - * {@link AuthorizationManagerServletRequestMatcherRegistry} that registers - * {@link MvcRequestMatcher}s. - * - *

- * Otherwise, use a configurer that registers {@link AntPathRequestMatcher}s. - * - *

- * When doing a path-based pattern, like `/path/*`, registered URIs should leave - * out the matching path. For example, if the target URI is `/path/resource/3`, - * then the configuration should look like this: - * .forServletPattern("/path/*", (path) -> path - * .requestMatchers("/resource/3").hasAuthority(...) - * ) - * - * - *

- * Or, if the pattern is `/path/subpath/*`, and the URI is - * `/path/subpath/resource/3`, then the configuration should look like this: - * - * .forServletPattern("/path/subpath/*", (path) -> path - * .requestMatchers("/resource/3").hasAuthority(...) - * ) - * - * - *

- * For all other patterns, please supply the URI in absolute terms. For example, - * if the target URI is `/js/**` and it matches to the default servlet, then the - * configuration should look like this: - * .forServletPattern("/", (root) -> root - * .requestMatchers("/js/**").hasAuthority(...) - * ) - * - * - *

- * Or, if the target URI is `/views/**`, and it matches to a `*.jsp` extension - * servlet, then the configuration should look like this: - * .forServletPattern("*.jsp", (jsp) -> jsp - * .requestMatchers("/views/**").hasAuthority(...) - * ) - * - * @param customizer a customizer that uses a - * {@link AuthorizationManagerServletRequestMatcherRegistry} for URIs mapped to - * the provided servlet - * @return an {@link AuthorizationManagerServletRequestMatcherRegistry} for - * further configurations - * @since 6.2 - */ - public AuthorizationManagerRequestMatcherRegistry forServletPattern(String pattern, - Customizer customizer) { - ApplicationContext context = getApplicationContext(); - RequestMatcherBuilder builder = RequestMatcherBuilders.createForServletPattern(context, pattern); - AuthorizationManagerServletRequestMatcherRegistry registry = new AuthorizationManagerServletRequestMatcherRegistry( - builder); - customizer.customize(registry); - this.servletPattern.put(pattern, registry); - return this; + return new AuthorizedUrl(requestMatchers); } /** @@ -299,25 +186,7 @@ public final class AuthorizeHttpRequestsConfigurer - * @Configuration - * @EnableWebSecurity - * public class SecurityConfig { - * - * @Bean - * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - * http - * .authorizeHttpRequests((authorize) -> authorize - * .dispatcherTypeMatchers(DispatcherType.ERROR).permitAll() - * // ... - * ); - * return http.build(); - * } - * } - * */ - @Deprecated(since = "6.1", forRemoval = true) public AuthorizationManagerRequestMatcherRegistry shouldFilterAllDispatcherTypes(boolean shouldFilter) { this.shouldFilterAllDispatcherTypes = shouldFilter; return this; @@ -327,272 +196,11 @@ public final class AuthorizeHttpRequestsConfigurer - * This class is designed primarily for use with the {@link HttpSecurity} DSL. For - * that reason, please use {@link HttpSecurity#authorizeHttpRequests} instead as - * it exposes this class fluently alongside related DSL configurations. - * - *

- * NOTE: In many cases, which kind of request matcher is needed is apparent by the - * servlet configuration, and so you should generally use the methods found in - * {@link AbstractRequestMatcherRegistry} instead of this these. Use this class - * when you want or need to indicate which request matcher URIs belong to which - * servlet. - * - *

- * In all cases, though, you may arrange your request matchers by servlet pattern - * with the {@link AuthorizationManagerRequestMatcherRegistry#forServletPattern} - * method in the {@link HttpSecurity#authorizeHttpRequests} DSL. - * - *

- * Consider, for example, the circumstance where you have Spring MVC configured - * and also Spring Boot H2 Console. Spring MVC registers a servlet of type - * {@link DispatcherServlet} as the default servlet and Spring Boot registers a - * servlet of its own as well at `/h2-console/*`. - * - *

- * Such might have a configuration like this in Spring Security: - * http - * .authorizeHttpRequests((authorize) -> authorize - * .requestMatchers("/js/**", "/css/**").permitAll() - * .requestMatchers("/my/controller/**").hasAuthority("CONTROLLER") - * .requestMatchers("/h2-console/**").hasAuthority("H2") - * ) - * // ... - * - * - *

- * Spring Security by default addresses the above configuration on its own. - * - *

- * However, consider the same situation, but where {@link DispatcherServlet} is - * mapped to a path like `/mvc/*`. In this case, the above configuration is - * ambiguous, and you should use this class to clarify the rest of each MVC URI - * like so: - * http - * .authorizeHttpRequests((authorize) -> authorize - * .forServletPattern("/", (root) -> root - * .requestMatchers("/js/**", "/css/**").permitAll() - * ) - * .forServletPattern("/mvc/*", (mvc) -> mvc - * .requestMatchers("/my/controller/**").hasAuthority("CONTROLLER") - * ) - * .forServletPattern("/h2-console/*", (h2) -> h2 - * .anyRequest().hasAuthority("OTHER") - * ) - * ) - * // ... - * - * - *

- * In the above configuration, it's now clear to Spring Security that the - * following matchers map to these corresponding URIs: - * - *

    - *
  • <default> + `/js/**` ==> `/js/**`
  • - *
  • <default> + `/css/**` ==> `/css/**`
  • - *
  • `/mvc` + `/my/controller/**` ==> - * `/mvc/my/controller/**`
  • - *
  • `/h2-console` + <any request> ==> - * `/h2-console/**`
  • - *
- * - * @author Josh Cummings - * @since 6.2 - * @see AbstractRequestMatcherRegistry - * @see AuthorizeHttpRequestsConfigurer - */ - public final class AuthorizationManagerServletRequestMatcherRegistry - extends AbstractRequestMatcherBuilderRegistry { - - private final RequestMatcherDelegatingAuthorizationManager.Builder managerBuilder = RequestMatcherDelegatingAuthorizationManager - .builder(); - - private List unmappedMatchers; - - AuthorizationManagerServletRequestMatcherRegistry(RequestMatcherBuilder builder) { - super(AuthorizationManagerRequestMatcherRegistry.this.getApplicationContext(), builder); - } - - AuthorizationManager authorizationManager() { - Assert.state(this.unmappedMatchers == null, - () -> "An incomplete mapping was found for " + this.unmappedMatchers - + ". Try completing it with something like requestUrls()..hasRole('USER')"); - AuthorizationManager request = this.managerBuilder.build(); - return (authentication, context) -> request.check(authentication, context.getRequest()); - } - - @Override - protected ServletAuthorizedUrl chainRequestMatchers(List requestMatchers) { - this.unmappedMatchers = requestMatchers; - return new ServletAuthorizedUrl((manager) -> addMapping(requestMatchers, manager)); - } - - private AuthorizationManagerServletRequestMatcherRegistry addMapping(List matchers, - AuthorizationManager manager) { - this.unmappedMatchers = null; - for (RequestMatcher matcher : matchers) { - this.managerBuilder.add(matcher, manager); - } - return this; - } - - } - - /** - * An object that allows configuring the {@link AuthorizationManager} for - * {@link RequestMatcher}s. - * - * @author Josh Cummings - * @since 6.2 - */ - public final class ServletAuthorizedUrl { - - private final Function, AuthorizationManagerServletRequestMatcherRegistry> registrar; - - ServletAuthorizedUrl( - Function, AuthorizationManagerServletRequestMatcherRegistry> registrar) { - this.registrar = registrar; - } - - /** - * Specify that URLs are allowed by anyone. - * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further - * customizations - */ - public AuthorizationManagerServletRequestMatcherRegistry permitAll() { - return access(permitAllAuthorizationManager); - } - - /** - * Specify that URLs are not allowed by anyone. - * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further - * customizations - */ - public AuthorizationManagerServletRequestMatcherRegistry denyAll() { - return access((a, o) -> new AuthorizationDecision(false)); - } - - /** - * Specifies a user requires a role. - * @param role the role that should be required which is prepended with ROLE_ - * automatically (i.e. USER, ADMIN, etc). It should not start with ROLE_ - * @return {@link AuthorizationManagerRequestMatcherRegistry} for further - * customizations - */ - public AuthorizationManagerServletRequestMatcherRegistry hasRole(String role) { - return access(withRoleHierarchy(AuthorityAuthorizationManager - .hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, new String[] { role }))); - } - - /** - * Specifies that a user requires one of many roles. - * @param roles the roles that the user should have at least one of (i.e. - * ADMIN, USER, etc). Each role should not start with ROLE_ since it is - * automatically prepended already - * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further - * customizations - */ - public AuthorizationManagerServletRequestMatcherRegistry hasAnyRole(String... roles) { - return access(withRoleHierarchy(AuthorityAuthorizationManager - .hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, roles))); - } - - /** - * Specifies a user requires an authority. - * @param authority the authority that should be required - * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further - * customizations - */ - public AuthorizationManagerServletRequestMatcherRegistry hasAuthority(String authority) { - return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority))); - } - - /** - * Specifies that a user requires one of many authorities. - * @param authorities the authorities that the user should have at least one - * of (i.e. ROLE_USER, ROLE_ADMIN, etc) - * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further - * customizations - */ - public AuthorizationManagerServletRequestMatcherRegistry hasAnyAuthority(String... authorities) { - return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities))); - } - - private AuthorityAuthorizationManager withRoleHierarchy( - AuthorityAuthorizationManager manager) { - manager.setRoleHierarchy(AuthorizeHttpRequestsConfigurer.this.roleHierarchy.get()); - return manager; - } - - /** - * Specify that URLs are allowed by any authenticated user. - * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further - * customizations - */ - public AuthorizationManagerServletRequestMatcherRegistry authenticated() { - return access(AuthenticatedAuthorizationManager.authenticated()); - } - - /** - * Specify that URLs are allowed by users who have authenticated and were not - * "remembered". - * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further - * customization - * @see RememberMeConfigurer - */ - public AuthorizationManagerServletRequestMatcherRegistry fullyAuthenticated() { - return access(AuthenticatedAuthorizationManager.fullyAuthenticated()); - } - - /** - * Specify that URLs are allowed by users that have been remembered. - * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further - * customization - * @since 5.8 - * @see RememberMeConfigurer - */ - public AuthorizationManagerServletRequestMatcherRegistry rememberMe() { - return access(AuthenticatedAuthorizationManager.rememberMe()); - } - - /** - * Specify that URLs are allowed by anonymous users. - * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further - * customization - * @since 5.8 - */ - public AuthorizationManagerServletRequestMatcherRegistry anonymous() { - return access(AuthenticatedAuthorizationManager.anonymous()); - } - - /** - * Allows specifying a custom {@link AuthorizationManager}. - * @param manager the {@link AuthorizationManager} to use - * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further - * customizations - */ - public AuthorizationManagerServletRequestMatcherRegistry access( - AuthorizationManager manager) { - Assert.notNull(manager, "manager cannot be null"); - return this.registrar.apply(manager); - } - - } - } /** @@ -603,11 +211,18 @@ public final class AuthorizeHttpRequestsConfigurer, AuthorizationManagerRequestMatcherRegistry> registrar; + private final List matchers; - AuthorizedUrl( - Function, AuthorizationManagerRequestMatcherRegistry> registrar) { - this.registrar = registrar; + /** + * Creates an instance. + * @param matchers the {@link RequestMatcher} instances to map + */ + AuthorizedUrl(List matchers) { + this.matchers = matchers; + } + + protected List getMatchers() { + return this.matchers; } /** @@ -636,8 +251,7 @@ public final class AuthorizeHttpRequestsConfigurer withRoleHierarchy( - AuthorityAuthorizationManager manager) { - manager.setRoleHierarchy(AuthorizeHttpRequestsConfigurer.this.roleHierarchy.get()); - return manager; + return access(AuthorityAuthorizationManager.hasAnyAuthority(authorities)); } /** @@ -731,7 +338,7 @@ public final class AuthorizeHttpRequestsConfigurer manager) { Assert.notNull(manager, "manager cannot be null"); - return this.registrar.apply(manager); + return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java index 241ef19476..56aa8f4d0f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import java.util.List; import org.springframework.context.ApplicationContext; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.SecurityConfigurer; @@ -195,10 +194,7 @@ public final class ChannelSecurityConfigurer> * Return the {@link SecurityBuilder} when done using the * {@link SecurityConfigurer}. This is useful for method chaining. * @return the type of {@link HttpSecurityBuilder} that is being configured - * @deprecated For removal in 7.0. Use - * {@link HttpSecurity#requiresChannel(Customizer)} instead */ - @Deprecated(since = "6.1", forRemoval = true) public H and() { return ChannelSecurityConfigurer.this.and(); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java index 55c8f81ab8..54009892e2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import org.springframework.security.web.csrf.CsrfLogoutHandler; import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRequestHandler; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; +import org.springframework.security.web.csrf.LazyCsrfTokenRepository; import org.springframework.security.web.csrf.MissingCsrfTokenException; import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler; import org.springframework.security.web.session.InvalidSessionStrategy; @@ -82,7 +83,7 @@ import org.springframework.util.Assert; public final class CsrfConfigurer> extends AbstractHttpConfigurer, H> { - private CsrfTokenRepository csrfTokenRepository = new HttpSessionCsrfTokenRepository(); + private CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository()); private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER; @@ -104,7 +105,7 @@ public final class CsrfConfigurer> /** * Specify the {@link CsrfTokenRepository} to use. The default is an - * {@link HttpSessionCsrfTokenRepository}. + * {@link HttpSessionCsrfTokenRepository} wrapped by {@link LazyCsrfTokenRepository}. * @param csrfTokenRepository the {@link CsrfTokenRepository} to use * @return the {@link CsrfConfigurer} for further customizations */ diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DispatcherServletDelegatingRequestMatcherBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DispatcherServletDelegatingRequestMatcherBuilder.java deleted file mode 100644 index bb300d5703..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DispatcherServletDelegatingRequestMatcherBuilder.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers; - -import jakarta.servlet.http.HttpServletRequest; - -import org.springframework.http.HttpMethod; -import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.Assert; - -final class DispatcherServletDelegatingRequestMatcherBuilder implements RequestMatcherBuilder { - - final MvcRequestMatcherBuilder mvc; - - final AntPathRequestMatcherBuilder ant; - - final ServletRegistrationCollection registrations; - - DispatcherServletDelegatingRequestMatcherBuilder(MvcRequestMatcherBuilder mvc, AntPathRequestMatcherBuilder ant, - ServletRegistrationCollection registrations) { - this.mvc = mvc; - this.ant = ant; - this.registrations = registrations; - } - - @Override - public RequestMatcher matcher(String pattern) { - MvcRequestMatcher mvc = this.mvc.matcher(pattern); - AntPathRequestMatcher ant = this.ant.matcher(pattern); - return new DispatcherServletDelegatingRequestMatcher(mvc, ant, this.registrations); - } - - @Override - public RequestMatcher matcher(HttpMethod method, String pattern) { - MvcRequestMatcher mvc = this.mvc.matcher(method, pattern); - AntPathRequestMatcher ant = this.ant.matcher(method, pattern); - return new DispatcherServletDelegatingRequestMatcher(mvc, ant, this.registrations); - } - - static final class DispatcherServletDelegatingRequestMatcher implements RequestMatcher { - - private final MvcRequestMatcher mvc; - - private final AntPathRequestMatcher ant; - - private final ServletRegistrationCollection registrations; - - private DispatcherServletDelegatingRequestMatcher(MvcRequestMatcher mvc, AntPathRequestMatcher ant, - ServletRegistrationCollection registrations) { - this.mvc = mvc; - this.ant = ant; - this.registrations = registrations; - } - - @Override - public boolean matches(HttpServletRequest request) { - String name = request.getHttpServletMapping().getServletName(); - ServletRegistrationCollection.Registration registration = this.registrations.registrationByName(name); - Assert.notNull(registration, - String.format("Could not find %s in servlet configuration %s", name, this.registrations)); - if (registration.isDispatcherServlet()) { - return this.mvc.matches(request); - } - return this.ant.matches(request); - } - - @Override - public MatchResult matcher(HttpServletRequest request) { - String name = request.getHttpServletMapping().getServletName(); - ServletRegistrationCollection.Registration registration = this.registrations.registrationByName(name); - Assert.notNull(registration, - String.format("Could not find %s in servlet configuration %s", name, this.registrations)); - if (registration.isDispatcherServlet()) { - return this.mvc.matcher(request); - } - return this.ant.matcher(request); - } - - @Override - public String toString() { - return String.format("DispatcherServlet [mvc=[%s], ant=[%s], servlet=[%s]]", this.mvc, this.ant, - this.registrations); - } - - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java index b506427d25..abcdf331d3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import java.util.Map; import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -136,13 +135,7 @@ public class HeadersConfigurer> * X-Content-Type-Options: nosniff * * @return the {@link ContentTypeOptionsConfig} for additional customizations - * @deprecated For removal in 7.0. Use {@link #contentTypeOptions(Customizer)} or - * {@code contentTypeOptions(Customizer.withDefaults())} to stick with defaults. See - * the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ContentTypeOptionsConfig contentTypeOptions() { return this.contentTypeOptions.enable(); } @@ -173,13 +166,7 @@ public class HeadersConfigurer> * >X-XSS-Protection header *

* @return the {@link XXssConfig} for additional customizations - * @deprecated For removal in 7.0. Use {@link #xssProtection(Customizer)} or - * {@code xssProtection(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public XXssConfig xssProtection() { return this.xssProtection.enable(); } @@ -210,13 +197,7 @@ public class HeadersConfigurer> *
  • Expires: 0
  • * * @return the {@link CacheControlConfig} for additional customizations - * @deprecated For removal in 7.0. Use {@link #cacheControl(Customizer)} or - * {@code cacheControl(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public CacheControlConfig cacheControl() { return this.cacheControl.enable(); } @@ -243,10 +224,7 @@ public class HeadersConfigurer> * HTTP Strict Transport Security * (HSTS). * @return the {@link HstsConfig} for additional customizations - * @deprecated For removal in 7.0. Use - * {@link #httpStrictTransportSecurity(Customizer)} instead */ - @Deprecated(since = "6.1", forRemoval = true) public HstsConfig httpStrictTransportSecurity() { return this.hsts.enable(); } @@ -267,13 +245,7 @@ public class HeadersConfigurer> /** * Allows customizing the {@link XFrameOptionsHeaderWriter}. * @return the {@link FrameOptionsConfig} for additional customizations - * @deprecated For removal in 7.0. Use {@link #frameOptions(Customizer)} or - * {@code frameOptions(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public FrameOptionsConfig frameOptions() { return this.frameOptions.enable(); } @@ -343,11 +315,8 @@ public class HeadersConfigurer> * @return the {@link ContentSecurityPolicyConfig} for additional configuration * @throws IllegalArgumentException if policyDirectives is null or empty * @since 4.1 - * @deprecated For removal in 7.0. Use {@link #contentSecurityPolicy(Customizer)} - * instead * @see ContentSecurityPolicyHeaderWriter */ - @Deprecated(since = "6.1", forRemoval = true) public ContentSecurityPolicyConfig contentSecurityPolicy(String policyDirectives) { this.contentSecurityPolicy.writer = new ContentSecurityPolicyHeaderWriter(policyDirectives); return this.contentSecurityPolicy; @@ -477,14 +446,8 @@ public class HeadersConfigurer> * * @return the {@link ReferrerPolicyConfig} for additional configuration * @since 4.2 - * @deprecated For removal in 7.0. Use {@link #referrerPolicy(Customizer)} or - * {@code referrerPolicy(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. * @see ReferrerPolicyHeaderWriter */ - @Deprecated(since = "6.1", forRemoval = true) public ReferrerPolicyConfig referrerPolicy() { this.referrerPolicy.writer = new ReferrerPolicyHeaderWriter(); return this.referrerPolicy; @@ -506,14 +469,8 @@ public class HeadersConfigurer> * @return the {@link ReferrerPolicyConfig} for additional configuration * @throws IllegalArgumentException if policy is null or empty * @since 4.2 - * @deprecated For removal in 7.0. Use {@link #referrerPolicy(Customizer)} or - * {@code referrerPolicy(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. * @see ReferrerPolicyHeaderWriter */ - @Deprecated(since = "6.1", forRemoval = true) public ReferrerPolicyConfig referrerPolicy(ReferrerPolicy policy) { this.referrerPolicy.writer = new ReferrerPolicyHeaderWriter(policy); return this.referrerPolicy; @@ -555,12 +512,8 @@ public class HeadersConfigurer> * @return the {@link FeaturePolicyConfig} for additional configuration * @throws IllegalArgumentException if policyDirectives is {@code null} or empty * @since 5.1 - * @deprecated For removal in 7.0. Use {@link #permissionsPolicy(Customizer)} or - * {@code permissionsPolicy(Customizer.withDefaults())} to stick with defaults. See - * the documentation - * for more details. - * @see ObjectPostProcessorConfiguration FeaturePolicyHeaderWriter + * @deprecated Use {@link #permissionsPolicy(Customizer)} instead. + * @seeObjectPostProcessorConfiguration FeaturePolicyHeaderWriter */ @Deprecated public FeaturePolicyConfig featurePolicy(String policyDirectives) { @@ -584,14 +537,8 @@ public class HeadersConfigurer> * * @return the {@link PermissionsPolicyConfig} for additional configuration * @since 5.5 - * @deprecated For removal in 7.0. Use {@link #permissionsPolicy(Customizer)} or - * {@code permissionsPolicy(Customizer.withDefaults())} to stick with defaults. See - * the documentation - * for more details. * @see PermissionsPolicyHeaderWriter */ - @Deprecated(since = "6.1", forRemoval = true) public PermissionsPolicyConfig permissionsPolicy() { this.permissionsPolicy.writer = new PermissionsPolicyHeaderWriter(); return this.permissionsPolicy; @@ -628,11 +575,8 @@ public class HeadersConfigurer> *

    * @return the {@link CrossOriginOpenerPolicyConfig} for additional confniguration * @since 5.7 - * @deprecated For removal in 7.0. Use {@link #crossOriginOpenerPolicy(Customizer)} - * instead * @see CrossOriginOpenerPolicyHeaderWriter */ - @Deprecated(since = "6.1", forRemoval = true) public CrossOriginOpenerPolicyConfig crossOriginOpenerPolicy() { this.crossOriginOpenerPolicy.writer = new CrossOriginOpenerPolicyHeaderWriter(); return this.crossOriginOpenerPolicy; @@ -672,11 +616,8 @@ public class HeadersConfigurer> *

    * @return the {@link CrossOriginEmbedderPolicyConfig} for additional customizations * @since 5.7 - * @deprecated For removal in 7.0. Use {@link #crossOriginEmbedderPolicy(Customizer)} - * instead * @see CrossOriginEmbedderPolicyHeaderWriter */ - @Deprecated(since = "6.1", forRemoval = true) public CrossOriginEmbedderPolicyConfig crossOriginEmbedderPolicy() { this.crossOriginEmbedderPolicy.writer = new CrossOriginEmbedderPolicyHeaderWriter(); return this.crossOriginEmbedderPolicy; @@ -716,11 +657,8 @@ public class HeadersConfigurer> *

    * @return the {@link HeadersConfigurer} for additional customizations * @since 5.7 - * @deprecated For removal in 7.0. Use {@link #crossOriginResourcePolicy(Customizer)} - * instead * @see CrossOriginResourcePolicyHeaderWriter */ - @Deprecated(since = "6.1", forRemoval = true) public CrossOriginResourcePolicyConfig crossOriginResourcePolicy() { this.crossOriginResourcePolicy.writer = new CrossOriginResourcePolicyHeaderWriter(); return this.crossOriginResourcePolicy; @@ -770,10 +708,7 @@ public class HeadersConfigurer> /** * Allows customizing the {@link HeadersConfigurer} * @return the {@link HeadersConfigurer} for additional customization - * @deprecated For removal in 7.0. Use {@link #contentTypeOptions(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeadersConfigurer and() { return HeadersConfigurer.this; } @@ -846,13 +781,7 @@ public class HeadersConfigurer> * Allows completing configuration of X-XSS-Protection and continuing * configuration of headers. * @return the {@link HeadersConfigurer} for additional configuration - * @deprecated For removal in 7.0. Use {@link #xssProtection(Customizer)} or - * {@code xssProtection(Customizer.withDefaults())} to stick with defaults. See - * the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public HeadersConfigurer and() { return HeadersConfigurer.this; } @@ -891,13 +820,7 @@ public class HeadersConfigurer> * Allows completing configuration of Cache Control and continuing configuration * of headers. * @return the {@link HeadersConfigurer} for additional configuration - * @deprecated For removal in 7.0. Use {@link #cacheControl(Customizer)} or - * {@code cacheControl(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public HeadersConfigurer and() { return HeadersConfigurer.this; } @@ -1003,10 +926,7 @@ public class HeadersConfigurer> * Allows completing configuration of Strict Transport Security and continuing * configuration of headers. * @return the {@link HeadersConfigurer} for additional configuration - * @deprecated For removal in 7.0. Use - * {@link #httpStrictTransportSecurity(Customizer)} instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeadersConfigurer and() { return HeadersConfigurer.this; } @@ -1067,13 +987,7 @@ public class HeadersConfigurer> /** * Allows continuing customizing the headers configuration. * @return the {@link HeadersConfigurer} for additional configuration - * @deprecated For removal in 7.0. Use {@link #frameOptions(Customizer)} or - * {@code frameOptions(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public HeadersConfigurer and() { return HeadersConfigurer.this; } @@ -1296,10 +1210,7 @@ public class HeadersConfigurer> * Allows completing configuration of Content Security Policy and continuing * configuration of headers. * @return the {@link HeadersConfigurer} for additional configuration - * @deprecated For removal in 7.0. Use {@link #contentSecurityPolicy(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeadersConfigurer and() { return HeadersConfigurer.this; } @@ -1324,14 +1235,6 @@ public class HeadersConfigurer> return this; } - /** - * @deprecated For removal in 7.0. Use {@link #referrerPolicy(Customizer)} or - * {@code referrerPolicy(Customizer.withDefaults())} to stick with defaults. See - * the documentation - * for more details. - */ - @Deprecated(since = "6.1", forRemoval = true) public HeadersConfigurer and() { return HeadersConfigurer.this; } @@ -1378,10 +1281,7 @@ public class HeadersConfigurer> * Allows completing configuration of Permissions Policy and continuing * configuration of headers. * @return the {@link HeadersConfigurer} for additional configuration - * @deprecated For removal in 7.0. Use {@link #permissionsPolicy(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeadersConfigurer and() { return HeadersConfigurer.this; } @@ -1411,10 +1311,7 @@ public class HeadersConfigurer> * Allows completing configuration of Cross Origin Opener Policy and continuing * configuration of headers. * @return the {@link HeadersConfigurer} for additional configuration - * @deprecated For removal in 7.0. Use - * {@link #crossOriginOpenerPolicy(Customizer)} instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeadersConfigurer and() { return HeadersConfigurer.this; } @@ -1445,10 +1342,7 @@ public class HeadersConfigurer> * Allows completing configuration of Cross-Origin-Embedder-Policy and continuing * configuration of headers. * @return the {@link HeadersConfigurer} for additional configuration - * @deprecated For removal in 7.0. Use - * {@link #crossOriginEmbedderPolicy(Customizer)} instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeadersConfigurer and() { return HeadersConfigurer.this; } @@ -1479,10 +1373,7 @@ public class HeadersConfigurer> * Allows completing configuration of Cross-Origin-Resource-Policy and continuing * configuration of headers. * @return the {@link HeadersConfigurer} for additional configuration - * @deprecated For removal in 7.0. Use - * {@link #crossOriginResourcePolicy(Customizer)} instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeadersConfigurer and() { return HeadersConfigurer.this; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java index c968508a2d..5ca0a5e2ec 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,6 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsS import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; -import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; @@ -77,7 +75,6 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy; * * * @author Rob Winch - * @author Evgeniy Cheban * @since 3.2 */ public final class HttpBasicConfigurer> @@ -94,8 +91,6 @@ public final class HttpBasicConfigurer> private BasicAuthenticationEntryPoint basicAuthEntryPoint = new BasicAuthenticationEntryPoint(); - private SecurityContextRepository securityContextRepository; - /** * Creates a new instance * @see HttpSecurity#httpBasic() @@ -147,19 +142,6 @@ public final class HttpBasicConfigurer> return this; } - /** - * Specifies a custom {@link SecurityContextRepository} to use for basic - * authentication. The default is {@link RequestAttributeSecurityContextRepository}. - * @param securityContextRepository the custom {@link SecurityContextRepository} to - * use - * @return {@link HttpBasicConfigurer} for additional customization - * @since 6.1 - */ - public HttpBasicConfigurer securityContextRepository(SecurityContextRepository securityContextRepository) { - this.securityContextRepository = securityContextRepository; - return this; - } - @Override public void init(B http) { registerDefaults(http); @@ -213,9 +195,6 @@ public final class HttpBasicConfigurer> if (this.authenticationDetailsSource != null) { basicAuthenticationFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource); } - if (this.securityContextRepository != null) { - basicAuthenticationFilter.setSecurityContextRepository(this.securityContextRepository); - } RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class); if (rememberMeServices != null) { basicAuthenticationFilter.setRememberMeServices(rememberMeServices); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/MvcRequestMatcherBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/MvcRequestMatcherBuilder.java deleted file mode 100644 index 60122c35c5..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/MvcRequestMatcherBuilder.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers; - -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpMethod; -import org.springframework.security.config.annotation.ObjectPostProcessor; -import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; - -final class MvcRequestMatcherBuilder implements RequestMatcherBuilder { - - private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector"; - - private final HandlerMappingIntrospector introspector; - - private final ObjectPostProcessor objectPostProcessor; - - private final String servletPath; - - private MvcRequestMatcherBuilder(ApplicationContext context, String servletPath) { - if (!context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { - throw new NoSuchBeanDefinitionException("A Bean named " + HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME - + " of type " + HandlerMappingIntrospector.class.getName() - + " is required to use MvcRequestMatcher. Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext."); - } - this.introspector = context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, HandlerMappingIntrospector.class); - this.objectPostProcessor = context.getBean(ObjectPostProcessor.class); - this.servletPath = servletPath; - } - - static MvcRequestMatcherBuilder absolute(ApplicationContext context) { - return new MvcRequestMatcherBuilder(context, null); - } - - static MvcRequestMatcherBuilder relativeTo(ApplicationContext context, String path) { - return new MvcRequestMatcherBuilder(context, path); - } - - @Override - public MvcRequestMatcher matcher(String pattern) { - MvcRequestMatcher matcher = new MvcRequestMatcher(this.introspector, pattern); - this.objectPostProcessor.postProcess(matcher); - if (this.servletPath != null) { - matcher.setServletPath(this.servletPath); - } - return matcher; - } - - @Override - public MvcRequestMatcher matcher(HttpMethod method, String pattern) { - MvcRequestMatcher matcher = new MvcRequestMatcher(this.introspector, pattern); - this.objectPostProcessor.postProcess(matcher); - matcher.setMethod(method); - if (this.servletPath != null) { - matcher.setServletPath(this.servletPath); - } - return matcher; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuilder.java deleted file mode 100644 index 6b95bf8111..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuilder.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.http.HttpMethod; -import org.springframework.security.web.util.matcher.AnyRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; - -/** - * An interface that abstracts how matchers are created - * - * @author Josh Cummings - * @since 6.2 - */ -interface RequestMatcherBuilder { - - /** - * Create a request matcher for the given pattern. - * - *

    - * For example, you might do something like the following: - * builder.matcher("/controller/**") - * - * @param pattern the pattern to use, typically an Ant path - * @return a {@link RequestMatcher} that matches on the given {@code pattern} - */ - RequestMatcher matcher(String pattern); - - /** - * Create a request matcher for the given pattern. - * - *

    - * For example, you might do something like the following: - * builder.matcher(HttpMethod.GET, "/controller/**") - * - * @param method the HTTP method to use - * @param pattern the pattern to use, typically an Ant path - * @return a {@link RequestMatcher} that matches on the given HTTP {@code method} and - * {@code pattern} - */ - RequestMatcher matcher(HttpMethod method, String pattern); - - /** - * Create a request matcher that matches any request - * @return a {@link RequestMatcher} that matches any request - */ - default RequestMatcher any() { - return AnyRequestMatcher.INSTANCE; - } - - /** - * Create an array request matchers, one for each of the given patterns. - * - *

    - * For example, you might do something like the following: - * builder.matcher("/controller-one/**", "/controller-two/**") - * - * @param patterns the patterns to use, typically Ant paths - * @return a list of {@link RequestMatcher} that match on the given {@code pattern} - */ - default List matchers(String... patterns) { - List matchers = new ArrayList<>(); - for (String pattern : patterns) { - matchers.add(matcher(pattern)); - } - return matchers; - } - - /** - * Create an array request matchers, one for each of the given patterns. - * - *

    - * For example, you might do something like the following: - * builder.matcher(HttpMethod.POST, "/controller-one/**", "/controller-two/**") - * - * @param method the HTTP method to use - * @param patterns the patterns to use, typically Ant paths - * @return a list of {@link RequestMatcher} that match on the given HTTP - * {@code method} and {@code pattern} - */ - default List matchers(HttpMethod method, String... patterns) { - List matchers = new ArrayList<>(); - for (String pattern : patterns) { - matchers.add(matcher(method, pattern)); - } - return matchers; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuilders.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuilders.java deleted file mode 100644 index f34793320e..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuilders.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpMethod; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.web.servlet.DispatcherServlet; - -/** - * A factory for constructing {@link RequestMatcherBuilder} instances - * - * @author Josh Cummings - * @since 6.2 - */ -final class RequestMatcherBuilders { - - private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector"; - - private static final String HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector"; - - private static final boolean mvcPresent; - - static { - mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR, RequestMatcherBuilders.class.getClassLoader()); - } - - private static final Log logger = LogFactory.getLog(RequestMatcherBuilders.class); - - private RequestMatcherBuilders() { - - } - - /** - * Create the default {@link RequestMatcherBuilder} for use by Spring Security DSLs. - * - *

    - * If Spring MVC is not present on the classpath or if there is no - * {@link DispatcherServlet}, this method will return an Ant-based builder. - * - *

    - * If the servlet configuration has only {@link DispatcherServlet} with a single - * mapping (for example `/` or `/path/*`), then this method will return an MVC-based - * builder. - * - *

    - * If the servlet configuration maps {@link DispatcherServlet} to a path and also has - * other servlets, this will throw an exception. In that case, an application should - * instead use the {@link RequestMatcherBuilders#createForServletPattern} ideally with - * the associated - * {@link org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer} - * to create builders by servlet path. - * - *

    - * Otherwise, (namely if {@link DispatcherServlet} is root), this method will return a - * builder that delegates to an Ant or Mvc builder at runtime. - * @param context the application context - * @return the appropriate {@link RequestMatcherBuilder} based on application - * configuration - */ - static RequestMatcherBuilder createDefault(ApplicationContext context) { - if (!mvcPresent) { - logger.trace("Defaulting to Ant matching since Spring MVC is not on the classpath"); - return AntPathRequestMatcherBuilder.absolute(); - } - if (!context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { - logger.trace("Defaulting to Ant matching since Spring MVC is not fully configured"); - return AntPathRequestMatcherBuilder.absolute(); - } - ServletRegistrationCollection registrations = ServletRegistrationCollection.registrations(context); - if (registrations.isEmpty()) { - logger.trace("Defaulting to MVC matching since Spring MVC is on the class path and no servlet " - + "information is available"); - return AntPathRequestMatcherBuilder.absolute(); - } - ServletRegistrationCollection dispatcherServlets = registrations.dispatcherServlets(); - if (dispatcherServlets.isEmpty()) { - logger.trace("Defaulting to Ant matching since there is no DispatcherServlet configured"); - return AntPathRequestMatcherBuilder.absolute(); - } - ServletRegistrationCollection.ServletPath servletPath = registrations.deduceOneServletPath(); - if (servletPath != null) { - String message = "Defaulting to MVC matching since DispatcherServlet [%s] is the only servlet mapping"; - logger.trace(String.format(message, servletPath.path())); - return MvcRequestMatcherBuilder.relativeTo(context, servletPath.path()); - } - servletPath = dispatcherServlets.deduceOneServletPath(); - if (servletPath == null) { - logger.trace("Did not choose a default since there is more than one DispatcherServlet mapping"); - String message = String.format(""" - This method cannot decide whether these patterns are Spring MVC patterns or not - since your servlet configuration has multiple Spring MVC servlet mappings. - - For your reference, here is your servlet configuration: %s - - To address this, you need to specify the servlet path for each endpoint. - You can use .forServletPattern in conjunction with requestMatchers do to this - like so: - - @Bean - SecurityFilterChain appSecurity(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests((authorize) -> authorize - .forServletPattern("/mvc-one/*", (one) -> one - .requestMatchers("/controller/**", "/endpoints/**" - )... - .forServletPattern("/mvc-two/*", (two) -> two - .requestMatchers("/other/**", "/controllers/**")... - ) - .forServletPattern("/h2-console/*", (h2) -> h2 - .requestMatchers("/**")... - ) - ) - // ... - return http.build(); - } - """, registrations); - return new ErrorRequestMatcherBuilder(message); - } - if (servletPath.path() != null) { - logger.trace("Did not choose a default since there is a non-root DispatcherServlet mapping"); - String message = String.format(""" - This method cannot decide whether these patterns are Spring MVC patterns or not - since your Spring MVC mapping is mapped to a path and you have other servlet mappings. - - For your reference, here is your servlet configuration: %s - - To address this, you need to specify the servlet path for each endpoint. - You can use .forServletPattern in conjunction with requestMatchers do to this - like so: - - @Bean - SecurityFilterChain appSecurity(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests((authorize) -> authorize - .forServletPattern("/mvc/*", (mvc) -> mvc - .requestMatchers("/controller/**", "/endpoints/**")... - ) - .forServletPattern("/h2-console/*", (h2) -> h2 - .requestMatchers("/**")... - ) - ) - // ... - return http.build(); - } - """, registrations); - return new ErrorRequestMatcherBuilder(message); - } - logger.trace("Defaulting to request-time checker since DispatcherServlet is mapped to root, but there are also " - + "other servlet mappings"); - return new DispatcherServletDelegatingRequestMatcherBuilder(MvcRequestMatcherBuilder.absolute(context), - AntPathRequestMatcherBuilder.absolute(), registrations); - } - - static RequestMatcherBuilder createForServletPattern(ApplicationContext context, String pattern) { - Assert.notNull(pattern, "pattern cannot be null"); - ServletRegistrationCollection registrations = ServletRegistrationCollection.registrations(context); - ServletRegistrationCollection.Registration registration = registrations.registrationByMapping(pattern); - Assert.notNull(registration, () -> String - .format("The given pattern %s doesn't seem to match any configured servlets: %s", pattern, registrations)); - boolean isPathPattern = pattern.startsWith("/") && pattern.endsWith("/*"); - if (isPathPattern) { - String path = pattern.substring(0, pattern.length() - 2); - return (registration.isDispatcherServlet()) ? MvcRequestMatcherBuilder.relativeTo(context, path) - : AntPathRequestMatcherBuilder.relativeTo(path); - } - return (registration.isDispatcherServlet()) ? MvcRequestMatcherBuilder.absolute(context) - : AntPathRequestMatcherBuilder.absolute(); - } - - private static class ErrorRequestMatcherBuilder implements RequestMatcherBuilder { - - private final String errorMessage; - - ErrorRequestMatcherBuilder(String errorMessage) { - this.errorMessage = errorMessage; - } - - @Override - public RequestMatcher matcher(String pattern) { - throw new IllegalArgumentException(this.errorMessage); - } - - @Override - public RequestMatcher matcher(HttpMethod method, String pattern) { - throw new IllegalArgumentException(this.errorMessage); - } - - @Override - public RequestMatcher any() { - throw new IllegalArgumentException(this.errorMessage); - } - - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletPatternRequestMatcher.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletPatternRequestMatcher.java deleted file mode 100644 index 989429a050..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletPatternRequestMatcher.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers; - -import jakarta.servlet.http.HttpServletRequest; - -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.Assert; - -final class ServletPatternRequestMatcher implements RequestMatcher { - - final String pattern; - - ServletPatternRequestMatcher(String pattern) { - Assert.notNull(pattern, "pattern cannot be null"); - this.pattern = pattern; - } - - @Override - public boolean matches(HttpServletRequest request) { - return this.pattern.equals(request.getHttpServletMapping().getPattern()); - } - - @Override - public String toString() { - return String.format("ServletPattern [pattern='%s']", this.pattern); - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletRegistrationCollection.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletRegistrationCollection.java deleted file mode 100644 index 560050c0ca..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletRegistrationCollection.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletRegistration; - -import org.springframework.context.ApplicationContext; -import org.springframework.util.ClassUtils; -import org.springframework.util.CollectionUtils; -import org.springframework.web.context.WebApplicationContext; - -final class ServletRegistrationCollection { - - private List registrations; - - private ServletRegistrationCollection() { - this.registrations = Collections.emptyList(); - } - - private ServletRegistrationCollection(List registrations) { - this.registrations = registrations; - } - - static ServletRegistrationCollection registrations(ApplicationContext context) { - if (!(context instanceof WebApplicationContext web)) { - return new ServletRegistrationCollection(); - } - ServletContext servletContext = web.getServletContext(); - if (servletContext == null) { - return new ServletRegistrationCollection(); - } - Map registrations = servletContext.getServletRegistrations(); - if (registrations == null) { - return new ServletRegistrationCollection(); - } - List filtered = new ArrayList<>(); - for (ServletRegistration registration : registrations.values()) { - Collection mappings = registration.getMappings(); - if (!CollectionUtils.isEmpty(mappings)) { - filtered.add(new Registration(registration)); - } - } - return new ServletRegistrationCollection(filtered); - } - - boolean isEmpty() { - return this.registrations.isEmpty(); - } - - Registration registrationByName(String name) { - for (Registration registration : this.registrations) { - if (registration.registration().getName().equals(name)) { - return registration; - } - } - return null; - } - - Registration registrationByMapping(String target) { - for (Registration registration : this.registrations) { - for (String mapping : registration.registration().getMappings()) { - if (target.equals(mapping)) { - return registration; - } - } - } - return null; - } - - ServletRegistrationCollection dispatcherServlets() { - List dispatcherServlets = new ArrayList<>(); - for (Registration registration : this.registrations) { - if (registration.isDispatcherServlet()) { - dispatcherServlets.add(registration); - } - } - return new ServletRegistrationCollection(dispatcherServlets); - } - - ServletPath deduceOneServletPath() { - if (this.registrations.size() > 1) { - return null; - } - ServletRegistration registration = this.registrations.iterator().next().registration(); - if (registration.getMappings().size() > 1) { - return null; - } - String mapping = registration.getMappings().iterator().next(); - if ("/".equals(mapping)) { - return new ServletPath(); - } - if (mapping.endsWith("/*")) { - return new ServletPath(mapping.substring(0, mapping.length() - 2)); - } - return null; - } - - @Override - public String toString() { - Map> mappings = new LinkedHashMap<>(); - for (Registration registration : this.registrations) { - mappings.put(registration.registration().getClassName(), registration.registration().getMappings()); - } - return mappings.toString(); - } - - record Registration(ServletRegistration registration) { - boolean isDispatcherServlet() { - Class dispatcherServlet = ClassUtils - .resolveClassName("org.springframework.web.servlet.DispatcherServlet", null); - try { - Class clazz = Class.forName(this.registration.getClassName()); - if (dispatcherServlet.isAssignableFrom(clazz)) { - return true; - } - } - catch (ClassNotFoundException ex) { - return false; - } - return false; - } - } - - record ServletPath(String path) { - ServletPath() { - this(null); - } - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index 504d68262c..b3a6fb163a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -296,7 +296,7 @@ public final class SessionManagementConfigurer> * @param sessionAuthenticationStrategy * @return the {@link SessionManagementConfigurer} for further customizations */ - public SessionManagementConfigurer addSessionAuthenticationStrategy( + SessionManagementConfigurer addSessionAuthenticationStrategy( SessionAuthenticationStrategy sessionAuthenticationStrategy) { this.sessionAuthenticationStrategies.add(sessionAuthenticationStrategy); return this; @@ -769,10 +769,7 @@ public final class SessionManagementConfigurer> /** * Used to chain back to the {@link SessionManagementConfigurer} * @return the {@link SessionManagementConfigurer} for further customizations - * @deprecated For removal in 7.0. Use {@link #sessionConcurrency(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public SessionManagementConfigurer and() { return SessionManagementConfigurer.this; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/DefaultOidcLogoutTokenValidatorFactory.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/DefaultOidcLogoutTokenValidatorFactory.java deleted file mode 100644 index fa7fe8e746..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/DefaultOidcLogoutTokenValidatorFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.oauth2.client; - -import java.util.function.Function; - -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtTimestampValidator; - -final class DefaultOidcLogoutTokenValidatorFactory implements Function> { - - @Override - public OAuth2TokenValidator apply(ClientRegistration clientRegistration) { - return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(), - new OidcBackChannelLogoutTokenValidator(clientRegistration)); - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java index 24b24909c9..04bc4f2c43 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.client; -import org.springframework.context.ApplicationContext; -import org.springframework.core.ResolvableType; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; @@ -138,10 +136,7 @@ public final class OAuth2ClientConfigurer> * Returns the {@link AuthorizationCodeGrantConfigurer} for configuring the OAuth 2.0 * Authorization Code Grant. * @return the {@link AuthorizationCodeGrantConfigurer} - * @deprecated For removal in 7.0. Use {@link #authorizationCodeGrant(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public AuthorizationCodeGrantConfigurer authorizationCodeGrant() { return this.authorizationCodeGrantConfigurer; } @@ -238,10 +233,7 @@ public final class OAuth2ClientConfigurer> /** * Returns the {@link OAuth2ClientConfigurer} for further configuration. * @return the {@link OAuth2ClientConfigurer} - * @deprecated For removal in 7.0. Use {@link #authorizationCodeGrant(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2ClientConfigurer and() { return OAuth2ClientConfigurer.this; } @@ -309,22 +301,7 @@ public final class OAuth2ClientConfigurer> if (this.accessTokenResponseClient != null) { return this.accessTokenResponseClient; } - ResolvableType resolvableType = ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class, - OAuth2AuthorizationCodeGrantRequest.class); - OAuth2AccessTokenResponseClient bean = getBeanOrNull(resolvableType); - return (bean != null) ? bean : new DefaultAuthorizationCodeTokenResponseClient(); - } - - @SuppressWarnings("unchecked") - private T getBeanOrNull(ResolvableType type) { - ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); - if (context != null) { - String[] names = context.getBeanNamesForType(type); - if (names.length == 1) { - return (T) context.getBean(names[0]); - } - } - return null; + return new DefaultAuthorizationCodeTokenResponseClient(); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java index 184042f823..bbf7fe7ef2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java @@ -25,8 +25,6 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; @@ -114,13 +112,4 @@ final class OAuth2ClientConfigurerUtils { return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null); } - static > OidcSessionRegistry getOidcSessionRegistry(B builder) { - OidcSessionRegistry sessionRegistry = builder.getSharedObject(OidcSessionRegistry.class); - if (sessionRegistry == null) { - sessionRegistry = new InMemoryOidcSessionRegistry(); - builder.setSharedObject(OidcSessionRegistry.class, sessionRegistry); - } - return sessionRegistry; - } - } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 8f479814a9..7efb7ec643 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,18 +22,9 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.GenericApplicationListenerAdapter; -import org.springframework.context.event.SmartApplicationListener; import org.springframework.core.ResolvableType; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.Customizer; @@ -41,14 +32,9 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer; -import org.springframework.security.context.DelegatingApplicationListener; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; -import org.springframework.security.core.session.AbstractSessionEvent; -import org.springframework.security.core.session.SessionDestroyedEvent; -import org.springframework.security.core.session.SessionIdChangedEvent; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider; import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; @@ -56,9 +42,6 @@ import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationC import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider; -import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -84,10 +67,7 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; -import org.springframework.security.web.authentication.session.SessionAuthenticationException; -import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; -import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -144,7 +124,6 @@ import org.springframework.util.ReflectionUtils; *

  • {@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not * configured and {@code DefaultLoginPageGeneratingFilter} is available, then a default * login page will be made available
  • - *
  • {@link OidcSessionRegistry}
  • * * * @author Joe Grandja @@ -223,26 +202,11 @@ public final class OAuth2LoginConfigurer> return this; } - /** - * Sets the registry for managing the OIDC client-provider session link - * @param oidcSessionRegistry the {@link OidcSessionRegistry} to use - * @return the {@link OAuth2LoginConfigurer} for further configuration - * @since 6.2 - */ - public OAuth2LoginConfigurer oidcSessionRegistry(OidcSessionRegistry oidcSessionRegistry) { - Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null"); - getBuilder().setSharedObject(OidcSessionRegistry.class, oidcSessionRegistry); - return this; - } - /** * Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization * Server's Authorization Endpoint. * @return the {@link AuthorizationEndpointConfig} - * @deprecated For removal in 7.0. Use {@link #authorizationEndpoint(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public AuthorizationEndpointConfig authorizationEndpoint() { return this.authorizationEndpointConfig; } @@ -263,13 +227,7 @@ public final class OAuth2LoginConfigurer> * Returns the {@link TokenEndpointConfig} for configuring the Authorization Server's * Token Endpoint. * @return the {@link TokenEndpointConfig} - * @deprecated For removal in 7.0. Use {@link #tokenEndpoint(Customizer)} or - * {@code tokenEndpoint(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public TokenEndpointConfig tokenEndpoint() { return this.tokenEndpointConfig; } @@ -290,10 +248,7 @@ public final class OAuth2LoginConfigurer> * Returns the {@link RedirectionEndpointConfig} for configuring the Client's * Redirection Endpoint. * @return the {@link RedirectionEndpointConfig} - * @deprecated For removal in 7.0. Use {@link #redirectionEndpoint(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public RedirectionEndpointConfig redirectionEndpoint() { return this.redirectionEndpointConfig; } @@ -314,13 +269,7 @@ public final class OAuth2LoginConfigurer> * Returns the {@link UserInfoEndpointConfig} for configuring the Authorization * Server's UserInfo Endpoint. * @return the {@link UserInfoEndpointConfig} - * @deprecated For removal in 7.0. Use {@link #userInfoEndpoint(Customizer)} or - * {@code userInfoEndpoint(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public UserInfoEndpointConfig userInfoEndpoint() { return this.userInfoEndpointConfig; } @@ -363,7 +312,10 @@ public final class OAuth2LoginConfigurer> super.init(http); } } - OAuth2AccessTokenResponseClient accessTokenResponseClient = getAccessTokenResponseClient(); + OAuth2AccessTokenResponseClient accessTokenResponseClient = this.tokenEndpointConfig.accessTokenResponseClient; + if (accessTokenResponseClient == null) { + accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient(); + } OAuth2UserService oauth2UserService = getOAuth2UserService(); OAuth2LoginAuthenticationProvider oauth2LoginAuthenticationProvider = new OAuth2LoginAuthenticationProvider( accessTokenResponseClient, oauth2UserService); @@ -430,7 +382,6 @@ public final class OAuth2LoginConfigurer> authenticationFilter .setAuthorizationRequestRepository(this.authorizationEndpointConfig.authorizationRequestRepository); } - configureOidcSessionRegistry(http); super.configure(http); } @@ -473,16 +424,6 @@ public final class OAuth2LoginConfigurer> return (!grantedAuthoritiesMapperMap.isEmpty() ? grantedAuthoritiesMapperMap.values().iterator().next() : null); } - private OAuth2AccessTokenResponseClient getAccessTokenResponseClient() { - if (this.tokenEndpointConfig.accessTokenResponseClient != null) { - return this.tokenEndpointConfig.accessTokenResponseClient; - } - ResolvableType resolvableType = ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class, - OAuth2AuthorizationCodeGrantRequest.class); - OAuth2AccessTokenResponseClient bean = getBeanOrNull(resolvableType); - return (bean != null) ? bean : new DefaultAuthorizationCodeTokenResponseClient(); - } - private OAuth2UserService getOidcUserService() { if (this.userInfoEndpointConfig.oidcUserService != null) { return this.userInfoEndpointConfig.oidcUserService; @@ -581,29 +522,6 @@ public final class OAuth2LoginConfigurer> return AnyRequestMatcher.INSTANCE; } - private void configureOidcSessionRegistry(B http) { - OidcSessionRegistry sessionRegistry = OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http); - SessionManagementConfigurer sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class); - if (sessionConfigurer != null) { - OidcSessionRegistryAuthenticationStrategy sessionAuthenticationStrategy = new OidcSessionRegistryAuthenticationStrategy(); - sessionAuthenticationStrategy.setSessionRegistry(sessionRegistry); - sessionConfigurer.addSessionAuthenticationStrategy(sessionAuthenticationStrategy); - } - OidcClientSessionEventListener listener = new OidcClientSessionEventListener(); - listener.setSessionRegistry(sessionRegistry); - registerDelegateApplicationListener(listener); - } - - private void registerDelegateApplicationListener(ApplicationListener delegate) { - DelegatingApplicationListener delegating = getBeanOrNull( - ResolvableType.forType(DelegatingApplicationListener.class)); - if (delegating == null) { - return; - } - SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate); - delegating.addListener(smartListener); - } - /** * Configuration options for the Authorization Server's Authorization Endpoint. */ @@ -673,10 +591,7 @@ public final class OAuth2LoginConfigurer> /** * Returns the {@link OAuth2LoginConfigurer} for further configuration. * @return the {@link OAuth2LoginConfigurer} - * @deprecated For removal in 7.0. Use {@link #authorizationEndpoint(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2LoginConfigurer and() { return OAuth2LoginConfigurer.this; } @@ -710,13 +625,7 @@ public final class OAuth2LoginConfigurer> /** * Returns the {@link OAuth2LoginConfigurer} for further configuration. * @return the {@link OAuth2LoginConfigurer} - * @deprecated For removal in 7.0. Use {@link #tokenEndpoint(Customizer)} or - * {@code tokenEndpoint(Customizer.withDefaults())} to stick with defaults. See - * the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2LoginConfigurer and() { return OAuth2LoginConfigurer.this; } @@ -748,10 +657,7 @@ public final class OAuth2LoginConfigurer> /** * Returns the {@link OAuth2LoginConfigurer} for further configuration. * @return the {@link OAuth2LoginConfigurer} - * @deprecated For removal in 7.0. Use {@link #redirectionEndpoint(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2LoginConfigurer and() { return OAuth2LoginConfigurer.this; } @@ -813,10 +719,7 @@ public final class OAuth2LoginConfigurer> /** * Returns the {@link OAuth2LoginConfigurer} for further configuration. * @return the {@link OAuth2LoginConfigurer} - * @deprecated For removal in 7.0. Use {@link #userInfoEndpoint(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2LoginConfigurer and() { return OAuth2LoginConfigurer.this; } @@ -851,88 +754,4 @@ public final class OAuth2LoginConfigurer> } - private static final class OidcClientSessionEventListener implements ApplicationListener { - - private final Log logger = LogFactory.getLog(OidcClientSessionEventListener.class); - - private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); - - /** - * {@inheritDoc} - */ - @Override - public void onApplicationEvent(AbstractSessionEvent event) { - if (event instanceof SessionDestroyedEvent destroyed) { - this.logger.debug("Received SessionDestroyedEvent"); - this.sessionRegistry.removeSessionInformation(destroyed.getId()); - return; - } - if (event instanceof SessionIdChangedEvent changed) { - this.logger.debug("Received SessionIdChangedEvent"); - OidcSessionInformation information = this.sessionRegistry - .removeSessionInformation(changed.getOldSessionId()); - if (information == null) { - this.logger - .debug("Failed to register new session id since old session id was not found in registry"); - return; - } - this.sessionRegistry.saveSessionInformation(information.withSessionId(changed.getNewSessionId())); - } - } - - /** - * The registry where OIDC Provider sessions are linked to the Client session. - * Defaults to in-memory storage. - * @param sessionRegistry the {@link OidcSessionRegistry} to use - */ - void setSessionRegistry(OidcSessionRegistry sessionRegistry) { - Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); - this.sessionRegistry = sessionRegistry; - } - - } - - private static final class OidcSessionRegistryAuthenticationStrategy implements SessionAuthenticationStrategy { - - private final Log logger = LogFactory.getLog(getClass()); - - private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); - - /** - * {@inheritDoc} - */ - @Override - public void onAuthentication(Authentication authentication, HttpServletRequest request, - HttpServletResponse response) throws SessionAuthenticationException { - HttpSession session = request.getSession(false); - if (session == null) { - return; - } - if (!(authentication.getPrincipal() instanceof OidcUser user)) { - return; - } - String sessionId = session.getId(); - CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); - Map headers = (csrfToken != null) ? Map.of(csrfToken.getHeaderName(), csrfToken.getToken()) - : Collections.emptyMap(); - OidcSessionInformation registration = new OidcSessionInformation(sessionId, headers, user); - if (this.logger.isTraceEnabled()) { - this.logger - .trace(String.format("Linking a provider [%s] session to this client's session", user.getIssuer())); - } - this.sessionRegistry.saveSessionInformation(registration); - } - - /** - * The registration for linking OIDC Provider Session information to the Client's - * session. Defaults to in-memory storage. - * @param sessionRegistry the {@link OidcSessionRegistry} to use - */ - void setSessionRegistry(OidcSessionRegistry sessionRegistry) { - Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); - this.sessionRegistry = sessionRegistry; - } - - } - } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthentication.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthentication.java deleted file mode 100644 index f65b1c11c0..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthentication.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.oauth2.client; - -import java.util.Collections; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; - -/** - * An {@link org.springframework.security.core.Authentication} implementation that - * represents the result of authenticating an OIDC Logout token for the purposes of - * performing Back-Channel Logout. - * - * @author Josh Cummings - * @since 6.2 - * @see OidcLogoutAuthenticationToken - * @see OIDC Back-Channel - * Logout - */ -class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken { - - private final OidcLogoutToken logoutToken; - - /** - * Construct an {@link OidcBackChannelLogoutAuthentication} - * @param logoutToken a deserialized, verified OIDC Logout Token - */ - OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) { - super(Collections.emptyList()); - this.logoutToken = logoutToken; - setAuthenticated(true); - } - - /** - * {@inheritDoc} - */ - @Override - public OidcLogoutToken getPrincipal() { - return this.logoutToken; - } - - /** - * {@inheritDoc} - */ - @Override - public OidcLogoutToken getCredentials() { - return this.logoutToken; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthenticationProvider.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthenticationProvider.java deleted file mode 100644 index d8a217f263..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthenticationProvider.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.oauth2.client; - -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.AuthenticationServiceException; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.jwt.BadJwtException; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtDecoderFactory; -import org.springframework.util.Assert; - -/** - * An {@link AuthenticationProvider} that authenticates an OIDC Logout Token; namely - * deserializing it, verifying its signature, and validating its claims. - * - *

    - * Intended to be included in a - * {@link org.springframework.security.authentication.ProviderManager} - * - * @author Josh Cummings - * @since 6.2 - * @see OidcLogoutAuthenticationToken - * @see org.springframework.security.authentication.ProviderManager - * @see OIDC Back-Channel - * Logout - */ -final class OidcBackChannelLogoutAuthenticationProvider implements AuthenticationProvider { - - private JwtDecoderFactory logoutTokenDecoderFactory; - - /** - * Construct an {@link OidcBackChannelLogoutAuthenticationProvider} - */ - OidcBackChannelLogoutAuthenticationProvider() { - OidcIdTokenDecoderFactory logoutTokenDecoderFactory = new OidcIdTokenDecoderFactory(); - logoutTokenDecoderFactory.setJwtValidatorFactory(new DefaultOidcLogoutTokenValidatorFactory()); - this.logoutTokenDecoderFactory = logoutTokenDecoderFactory; - } - - /** - * {@inheritDoc} - */ - @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { - if (!(authentication instanceof OidcLogoutAuthenticationToken token)) { - return null; - } - String logoutToken = token.getLogoutToken(); - ClientRegistration registration = token.getClientRegistration(); - Jwt jwt = decode(registration, logoutToken); - OidcLogoutToken oidcLogoutToken = OidcLogoutToken.withTokenValue(logoutToken) - .claims((claims) -> claims.putAll(jwt.getClaims())) - .build(); - return new OidcBackChannelLogoutAuthentication(oidcLogoutToken); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean supports(Class authentication) { - return OidcLogoutAuthenticationToken.class.isAssignableFrom(authentication); - } - - private Jwt decode(ClientRegistration registration, String token) { - JwtDecoder logoutTokenDecoder = this.logoutTokenDecoderFactory.createDecoder(registration); - try { - return logoutTokenDecoder.decode(token); - } - catch (BadJwtException failed) { - OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, failed.getMessage(), - "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation"); - throw new OAuth2AuthenticationException(error, failed); - } - catch (Exception failed) { - throw new AuthenticationServiceException(failed.getMessage(), failed); - } - } - - /** - * Use this {@link JwtDecoderFactory} to generate {@link JwtDecoder}s that correspond - * to the {@link ClientRegistration} associated with the OIDC logout token. - * @param logoutTokenDecoderFactory the {@link JwtDecoderFactory} to use - */ - void setLogoutTokenDecoderFactory(JwtDecoderFactory logoutTokenDecoderFactory) { - Assert.notNull(logoutTokenDecoderFactory, "logoutTokenDecoderFactory cannot be null"); - this.logoutTokenDecoderFactory = logoutTokenDecoderFactory; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutFilter.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutFilter.java deleted file mode 100644 index 0a03ec8383..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutFilter.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.oauth2.client; - -import java.io.IOException; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationServiceException; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; -import org.springframework.security.web.authentication.AuthenticationConverter; -import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.util.Assert; -import org.springframework.web.filter.OncePerRequestFilter; - -/** - * A filter for the Client-side OIDC Back-Channel Logout endpoint - * - * @author Josh Cummings - * @since 6.2 - * @see OIDC Back-Channel Logout - * Spec - */ -class OidcBackChannelLogoutFilter extends OncePerRequestFilter { - - private final Log logger = LogFactory.getLog(getClass()); - - private final AuthenticationConverter authenticationConverter; - - private final AuthenticationManager authenticationManager; - - private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter(); - - private LogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(); - - /** - * Construct an {@link OidcBackChannelLogoutFilter} - * @param authenticationConverter the {@link AuthenticationConverter} for deriving - * Logout Token authentication - * @param authenticationManager the {@link AuthenticationManager} for authenticating - * Logout Tokens - */ - OidcBackChannelLogoutFilter(AuthenticationConverter authenticationConverter, - AuthenticationManager authenticationManager) { - Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); - Assert.notNull(authenticationManager, "authenticationManager cannot be null"); - this.authenticationConverter = authenticationConverter; - this.authenticationManager = authenticationManager; - } - - /** - * {@inheritDoc} - */ - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { - Authentication token; - try { - token = this.authenticationConverter.convert(request); - } - catch (AuthenticationServiceException ex) { - this.logger.debug("Failed to process OIDC Back-Channel Logout", ex); - throw ex; - } - catch (AuthenticationException ex) { - handleAuthenticationFailure(response, ex); - return; - } - if (token == null) { - chain.doFilter(request, response); - return; - } - Authentication authentication; - try { - authentication = this.authenticationManager.authenticate(token); - } - catch (AuthenticationServiceException ex) { - this.logger.debug("Failed to process OIDC Back-Channel Logout", ex); - throw ex; - } - catch (AuthenticationException ex) { - handleAuthenticationFailure(response, ex); - return; - } - this.logoutHandler.logout(request, response, authentication); - } - - private void handleAuthenticationFailure(HttpServletResponse response, Exception ex) throws IOException { - this.logger.debug("Failed to process OIDC Back-Channel Logout", ex); - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - this.errorHttpMessageConverter.write(oauth2Error(ex), null, new ServletServerHttpResponse(response)); - } - - private OAuth2Error oauth2Error(Exception ex) { - if (ex instanceof OAuth2AuthenticationException oauth2) { - return oauth2.getError(); - } - return new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, ex.getMessage(), - "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation"); - } - - /** - * The strategy for expiring all Client sessions indicated by the logout request. - * Defaults to {@link OidcBackChannelLogoutHandler}. - * @param logoutHandler the {@link LogoutHandler} to use - */ - void setLogoutHandler(LogoutHandler logoutHandler) { - Assert.notNull(logoutHandler, "logoutHandler cannot be null"); - this.logoutHandler = logoutHandler; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandler.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandler.java deleted file mode 100644 index 21f6a9d60b..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandler.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.oauth2.client; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; -import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.util.Assert; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestOperations; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.UriComponentsBuilder; - -/** - * A {@link LogoutHandler} that locates the sessions associated with a given OIDC - * Back-Channel Logout Token and invalidates each one. - * - * @author Josh Cummings - * @since 6.2 - * @see OIDC Back-Channel Logout - * Spec - */ -final class OidcBackChannelLogoutHandler implements LogoutHandler { - - private final Log logger = LogFactory.getLog(getClass()); - - private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); - - private RestOperations restOperations = new RestTemplate(); - - private String logoutEndpointName = "/logout"; - - private String sessionCookieName = "JSESSIONID"; - - private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter(); - - @Override - public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { - if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) { - if (this.logger.isDebugEnabled()) { - String message = "Did not perform OIDC Back-Channel Logout since authentication [%s] was of the wrong type"; - this.logger.debug(String.format(message, authentication.getClass().getSimpleName())); - } - return; - } - Iterable sessions = this.sessionRegistry.removeSessionInformation(token.getPrincipal()); - Collection errors = new ArrayList<>(); - int totalCount = 0; - int invalidatedCount = 0; - for (OidcSessionInformation session : sessions) { - totalCount++; - try { - eachLogout(request, session); - invalidatedCount++; - } - catch (RestClientException ex) { - this.logger.debug("Failed to invalidate session", ex); - errors.add(ex.getMessage()); - this.sessionRegistry.saveSessionInformation(session); - } - } - if (this.logger.isTraceEnabled()) { - this.logger.trace(String.format("Invalidated %d out of %d sessions", invalidatedCount, totalCount)); - } - if (!errors.isEmpty()) { - handleLogoutFailure(response, oauth2Error(errors)); - } - } - - private void eachLogout(HttpServletRequest request, OidcSessionInformation session) { - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId()); - for (Map.Entry credential : session.getAuthorities().entrySet()) { - headers.add(credential.getKey(), credential.getValue()); - } - String url = request.getRequestURL().toString(); - String logout = UriComponentsBuilder.fromHttpUrl(url) - .replacePath(this.logoutEndpointName) - .build() - .toUriString(); - HttpEntity entity = new HttpEntity<>(null, headers); - this.restOperations.postForEntity(logout, entity, Object.class); - } - - private OAuth2Error oauth2Error(Collection errors) { - return new OAuth2Error("partial_logout", "not all sessions were terminated: " + errors, - "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation"); - } - - private void handleLogoutFailure(HttpServletResponse response, OAuth2Error error) { - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - try { - this.errorHttpMessageConverter.write(error, null, new ServletServerHttpResponse(response)); - } - catch (IOException ex) { - throw new IllegalStateException(ex); - } - } - - /** - * Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that - * this class uses - * {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify - * sessions. - * @param sessionRegistry the {@link OidcSessionRegistry} to use - */ - void setSessionRegistry(OidcSessionRegistry sessionRegistry) { - Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); - this.sessionRegistry = sessionRegistry; - } - - /** - * Use this {@link RestOperations} to perform the per-session back-channel logout - * @param restOperations the {@link RestOperations} to use - */ - void setRestOperations(RestOperations restOperations) { - Assert.notNull(restOperations, "restOperations cannot be null"); - this.restOperations = restOperations; - } - - /** - * Use this logout URI for performing per-session logout. Defaults to {@code /logout} - * since that is the default URI for - * {@link org.springframework.security.web.authentication.logout.LogoutFilter}. - * @param logoutUri the URI to use - */ - void setLogoutUri(String logoutUri) { - Assert.hasText(logoutUri, "logoutUri cannot be empty"); - this.logoutEndpointName = logoutUri; - } - - /** - * Use this cookie name for the session identifier. Defaults to {@code JSESSIONID}. - * - *

    - * Note that if you are using Spring Session, this likely needs to change to SESSION. - * @param sessionCookieName the cookie name to use - */ - void setSessionCookieName(String sessionCookieName) { - Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty"); - this.sessionCookieName = sessionCookieName; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutTokenValidator.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutTokenValidator.java deleted file mode 100644 index 7b6634f933..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutTokenValidator.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.oauth2.client; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimAccessor; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; -import org.springframework.security.oauth2.jwt.Jwt; - -/** - * A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance - * with the OIDC Back-Channel Logout Spec. - * - * @author Josh Cummings - * @since 6.2 - * @see OidcLogoutToken - * @see Logout - * Token - * @see the OIDC - * Back-Channel Logout spec - */ -final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator { - - private static final String LOGOUT_VALIDATION_URL = "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation"; - - private static final String BACK_CHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout"; - - private final String audience; - - private final String issuer; - - OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) { - this.audience = clientRegistration.getClientId(); - this.issuer = clientRegistration.getProviderDetails().getIssuerUri(); - } - - @Override - public OAuth2TokenValidatorResult validate(Jwt jwt) { - Collection errors = new ArrayList<>(); - - LogoutTokenClaimAccessor logoutClaims = jwt::getClaims; - Map events = logoutClaims.getEvents(); - if (events == null) { - errors.add(invalidLogoutToken("events claim must not be null")); - } - else if (events.get(BACK_CHANNEL_LOGOUT_EVENT) == null) { - errors.add(invalidLogoutToken("events claim map must contain \"" + BACK_CHANNEL_LOGOUT_EVENT + "\" key")); - } - - String issuer = logoutClaims.getIssuer().toExternalForm(); - if (issuer == null) { - errors.add(invalidLogoutToken("iss claim must not be null")); - } - else if (!this.issuer.equals(issuer)) { - errors.add(invalidLogoutToken( - "iss claim value must match `ClientRegistration#getProviderDetails#getIssuerUri`")); - } - - List audience = logoutClaims.getAudience(); - if (audience == null) { - errors.add(invalidLogoutToken("aud claim must not be null")); - } - else if (!audience.contains(this.audience)) { - errors.add(invalidLogoutToken("aud claim value must include `ClientRegistration#getClientId`")); - } - - Instant issuedAt = logoutClaims.getIssuedAt(); - if (issuedAt == null) { - errors.add(invalidLogoutToken("iat claim must not be null")); - } - - String jwtId = logoutClaims.getId(); - if (jwtId == null) { - errors.add(invalidLogoutToken("jti claim must not be null")); - } - - if (logoutClaims.getSubject() == null && logoutClaims.getSessionId() == null) { - errors.add(invalidLogoutToken("sub and sid claims must not both be null")); - } - - if (logoutClaims.getClaim("nonce") != null) { - errors.add(invalidLogoutToken("nonce claim must not be present")); - } - - return OAuth2TokenValidatorResult.failure(errors); - } - - private static OAuth2Error invalidLogoutToken(String description) { - return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, description, LOGOUT_VALIDATION_URL); - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutAuthenticationConverter.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutAuthenticationConverter.java deleted file mode 100644 index 2809991894..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutAuthenticationConverter.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.oauth2.client; - -import jakarta.servlet.http.HttpServletRequest; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.web.authentication.AuthenticationConverter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.Assert; - -/** - * An {@link AuthenticationConverter} that extracts the OIDC Logout Token authentication - * request - * - * @author Josh Cummings - * @since 6.2 - */ -final class OidcLogoutAuthenticationConverter implements AuthenticationConverter { - - private static final String DEFAULT_LOGOUT_URI = "/logout/connect/back-channel/{registrationId}"; - - private final Log logger = LogFactory.getLog(getClass()); - - private final ClientRegistrationRepository clientRegistrationRepository; - - private RequestMatcher requestMatcher = new AntPathRequestMatcher(DEFAULT_LOGOUT_URI, "POST"); - - OidcLogoutAuthenticationConverter(ClientRegistrationRepository clientRegistrationRepository) { - Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); - this.clientRegistrationRepository = clientRegistrationRepository; - } - - @Override - public Authentication convert(HttpServletRequest request) { - RequestMatcher.MatchResult result = this.requestMatcher.matcher(request); - if (!result.isMatch()) { - return null; - } - String registrationId = result.getVariables().get("registrationId"); - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); - if (clientRegistration == null) { - this.logger.debug("Did not process OIDC Back-Channel Logout since no ClientRegistration was found"); - throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); - } - String logoutToken = request.getParameter("logout_token"); - if (logoutToken == null) { - this.logger.debug("Failed to process OIDC Back-Channel Logout since no logout token was found"); - throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); - } - return new OidcLogoutAuthenticationToken(logoutToken, clientRegistration); - } - - /** - * The logout endpoint. Defaults to - * {@code /logout/connect/back-channel/{registrationId}}. - * @param requestMatcher the {@link RequestMatcher} to use - */ - void setRequestMatcher(RequestMatcher requestMatcher) { - Assert.notNull(requestMatcher, "requestMatcher cannot be null"); - this.requestMatcher = requestMatcher; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutAuthenticationToken.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutAuthenticationToken.java deleted file mode 100644 index 4a227e3be8..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutAuthenticationToken.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.oauth2.client; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.oauth2.client.registration.ClientRegistration; - -/** - * An {@link org.springframework.security.core.Authentication} instance that represents a - * request to authenticate an OIDC Logout Token. - * - * @author Josh Cummings - * @since 6.2 - */ -class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken { - - private final String logoutToken; - - private final ClientRegistration clientRegistration; - - /** - * Construct an {@link OidcLogoutAuthenticationToken} - * @param logoutToken a signed, serialized OIDC Logout token - * @param clientRegistration the {@link ClientRegistration client} associated with - * this token; this is usually derived from material in the logout HTTP request - */ - OidcLogoutAuthenticationToken(String logoutToken, ClientRegistration clientRegistration) { - super(AuthorityUtils.NO_AUTHORITIES); - this.logoutToken = logoutToken; - this.clientRegistration = clientRegistration; - } - - /** - * {@inheritDoc} - */ - @Override - public String getCredentials() { - return this.logoutToken; - } - - /** - * {@inheritDoc} - */ - @Override - public String getPrincipal() { - return this.logoutToken; - } - - /** - * Get the signed, serialized OIDC Logout token - * @return the logout token - */ - String getLogoutToken() { - return this.logoutToken; - } - - /** - * Get the {@link ClientRegistration} associated with this logout token - * @return the {@link ClientRegistration} - */ - ClientRegistration getClientRegistration() { - return this.clientRegistration; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java deleted file mode 100644 index b22e7e5a25..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurer.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.oauth2.client; - -import java.util.function.Consumer; - -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.ProviderManager; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.web.HttpSecurityBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.web.authentication.AuthenticationConverter; -import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.security.web.csrf.CsrfFilter; -import org.springframework.util.Assert; - -/** - * An {@link AbstractHttpConfigurer} for OIDC Logout flows - * - *

    - * OIDC Logout provides an application with the capability to have users log out by using - * their existing account at an OAuth 2.0 or OpenID Connect 1.0 Provider. - * - * - *

    Security Filters

    - * - * The following {@code Filter} is populated: - * - *
      - *
    • {@link OidcBackChannelLogoutFilter}
    • - *
    - * - *

    Shared Objects Used

    - * - * The following shared objects are used: - * - *
      - *
    • {@link ClientRegistrationRepository}
    • - *
    - * - * @author Josh Cummings - * @since 6.2 - * @see HttpSecurity#oidcLogout() - * @see OidcBackChannelLogoutFilter - * @see ClientRegistrationRepository - */ -public final class OidcLogoutConfigurer> - extends AbstractHttpConfigurer, B> { - - private BackChannelLogoutConfigurer backChannel; - - /** - * Sets the repository of client registrations. - * @param clientRegistrationRepository the repository of client registrations - * @return the {@link OAuth2LoginConfigurer} for further configuration - */ - public OidcLogoutConfigurer clientRegistrationRepository( - ClientRegistrationRepository clientRegistrationRepository) { - Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); - this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository); - return this; - } - - /** - * Sets the registry for managing the OIDC client-provider session link - * @param oidcSessionRegistry the {@link OidcSessionRegistry} to use - * @return the {@link OAuth2LoginConfigurer} for further configuration - */ - public OidcLogoutConfigurer oidcSessionRegistry(OidcSessionRegistry oidcSessionRegistry) { - Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null"); - getBuilder().setSharedObject(OidcSessionRegistry.class, oidcSessionRegistry); - return this; - } - - /** - * Configure OIDC Back-Channel Logout using the provided {@link Consumer} - * @return the {@link OidcLogoutConfigurer} for further configuration - */ - public OidcLogoutConfigurer backChannel(Customizer backChannelLogoutConfigurer) { - if (this.backChannel == null) { - this.backChannel = new BackChannelLogoutConfigurer(); - } - backChannelLogoutConfigurer.customize(this.backChannel); - return this; - } - - @Deprecated(forRemoval = true, since = "6.2") - public B and() { - return getBuilder(); - } - - @Override - public void configure(B builder) throws Exception { - if (this.backChannel != null) { - this.backChannel.configure(builder); - } - } - - /** - * A configurer for configuring OIDC Back-Channel Logout - */ - public final class BackChannelLogoutConfigurer { - - private AuthenticationConverter authenticationConverter; - - private final AuthenticationManager authenticationManager = new ProviderManager( - new OidcBackChannelLogoutAuthenticationProvider()); - - private LogoutHandler logoutHandler; - - private AuthenticationConverter authenticationConverter(B http) { - if (this.authenticationConverter == null) { - ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils - .getClientRegistrationRepository(http); - this.authenticationConverter = new OidcLogoutAuthenticationConverter(clientRegistrationRepository); - } - return this.authenticationConverter; - } - - private AuthenticationManager authenticationManager() { - return this.authenticationManager; - } - - private LogoutHandler logoutHandler(B http) { - if (this.logoutHandler == null) { - OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(); - logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http)); - this.logoutHandler = logoutHandler; - } - return this.logoutHandler; - } - - void configure(B http) { - OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(authenticationConverter(http), - authenticationManager()); - filter.setLogoutHandler(logoutHandler(http)); - http.addFilterBefore(filter, CsrfFilter.class); - } - - } - -} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index 01411c927c..2729637348 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -198,13 +198,6 @@ public final class OAuth2ResourceServerConfigurerdocumentation - * for more details. - */ - @Deprecated(since = "6.1", forRemoval = true) public JwtConfigurer jwt() { if (this.jwtConfigurer == null) { this.jwtConfigurer = new JwtConfigurer(this.context); @@ -226,14 +219,6 @@ public final class OAuth2ResourceServerConfigurerdocumentation - * for more details. - */ - @Deprecated(since = "6.1", forRemoval = true) public OpaqueTokenConfigurer opaqueToken() { if (this.opaqueTokenConfigurer == null) { this.opaqueTokenConfigurer = new OpaqueTokenConfigurer(this.context); @@ -409,13 +394,6 @@ public final class OAuth2ResourceServerConfigurerdocumentation - * for more details. - */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2ResourceServerConfigurer and() { return OAuth2ResourceServerConfigurer.this; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index 468afafad4..19639b7ac5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java @@ -33,8 +33,9 @@ import org.springframework.security.saml2.provider.service.authentication.Abstra import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository; -import org.springframework.security.saml2.provider.service.web.OpenSamlAuthenticationTokenConverter; +import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter; @@ -52,7 +53,6 @@ import org.springframework.security.web.util.matcher.NegatedRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatchers; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -115,9 +115,7 @@ public final class Saml2LoginConfigurer> private Saml2AuthenticationRequestResolver authenticationRequestResolver; - private RequestMatcher loginProcessingUrl = RequestMatchers.anyOf( - new AntPathRequestMatcher(Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI), - new AntPathRequestMatcher("/login/saml2/sso")); + private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; @@ -217,7 +215,7 @@ public final class Saml2LoginConfigurer> @Override public Saml2LoginConfigurer loginProcessingUrl(String loginProcessingUrl) { Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty"); - this.loginProcessingUrl = new AntPathRequestMatcher(loginProcessingUrl); + this.loginProcessingUrl = loginProcessingUrl; return this; } @@ -243,11 +241,12 @@ public final class Saml2LoginConfigurer> public void init(B http) throws Exception { registerDefaultCsrfOverride(http); relyingPartyRegistrationRepository(http); - this.saml2WebSsoAuthenticationFilter = new Saml2WebSsoAuthenticationFilter(getAuthenticationConverter(http)); + this.saml2WebSsoAuthenticationFilter = new Saml2WebSsoAuthenticationFilter(getAuthenticationConverter(http), + this.loginProcessingUrl); this.saml2WebSsoAuthenticationFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy()); - this.saml2WebSsoAuthenticationFilter.setRequiresAuthenticationRequestMatcher(this.loginProcessingUrl); setAuthenticationRequestRepository(http, this.saml2WebSsoAuthenticationFilter); setAuthenticationFilter(this.saml2WebSsoAuthenticationFilter); + super.loginProcessingUrl(this.loginProcessingUrl); if (StringUtils.hasText(this.loginPage)) { // Set custom login page super.loginPage(this.loginPage); @@ -293,6 +292,11 @@ public final class Saml2LoginConfigurer> } } + private RelyingPartyRegistrationResolver relyingPartyRegistrationResolver(B http) { + RelyingPartyRegistrationRepository registrations = relyingPartyRegistrationRepository(http); + return new DefaultRelyingPartyRegistrationResolver(registrations); + } + RelyingPartyRegistrationRepository relyingPartyRegistrationRepository(B http) { if (this.relyingPartyRegistrationRepository == null) { this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class); @@ -335,7 +339,7 @@ public final class Saml2LoginConfigurer> return bean; } OpenSaml4AuthenticationRequestResolver openSaml4AuthenticationRequestResolver = new OpenSaml4AuthenticationRequestResolver( - relyingPartyRegistrationRepository(http)); + relyingPartyRegistrationResolver(http)); openSaml4AuthenticationRequestResolver .setRequestMatcher(new AntPathRequestMatcher(this.authenticationRequestUri)); return openSaml4AuthenticationRequestResolver; @@ -348,14 +352,10 @@ public final class Saml2LoginConfigurer> AuthenticationConverter authenticationConverterBean = getBeanOrNull(http, Saml2AuthenticationTokenConverter.class); if (authenticationConverterBean == null) { - authenticationConverterBean = getBeanOrNull(http, OpenSamlAuthenticationTokenConverter.class); - } - if (authenticationConverterBean == null) { - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter( - this.relyingPartyRegistrationRepository); - converter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http)); - converter.setRequestMatcher(this.loginProcessingUrl); - return converter; + Assert.state(this.loginProcessingUrl.contains("{registrationId}"), + "loginProcessingUrl must contain {registrationId} path variable"); + return new Saml2AuthenticationTokenConverter( + new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository)); } return authenticationConverterBean; } @@ -372,7 +372,7 @@ public final class Saml2LoginConfigurer> if (csrf == null) { return; } - csrf.ignoringRequestMatchers(this.loginProcessingUrl); + csrf.ignoringRequestMatchers(new AntPathRequestMatcher(this.loginProcessingUrl)); } private void initDefaultLoginFilter(B http) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java index bbf448a394..f3e6c5e96e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,10 +39,11 @@ import org.springframework.security.saml2.provider.service.authentication.logout import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; +import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver; -import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutRequestValidatorParametersResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver; @@ -168,13 +169,7 @@ public final class Saml2LogoutConfigurer> /** * Get configurer for SAML 2.0 Logout Request components * @return the {@link LogoutRequestConfigurer} for further customizations - * @deprecated For removal in 7.0. Use {@link #logoutRequest(Customizer)} or - * {@code logoutRequest(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public LogoutRequestConfigurer logoutRequest() { return this.logoutRequestConfigurer; } @@ -194,19 +189,13 @@ public final class Saml2LogoutConfigurer> /** * Get configurer for SAML 2.0 Logout Response components * @return the {@link LogoutResponseConfigurer} for further customizations - * @deprecated For removal in 7.0. Use {@link #logoutResponse(Customizer)} or - * {@code logoutResponse(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public LogoutResponseConfigurer logoutResponse() { return this.logoutResponseConfigurer; } /** - * Configures SAML 2.0 Logout Response components + * Configures SAML 2.0 Logout Request components * @param logoutResponseConfigurerCustomizer the {@link Customizer} to provide more * options for the {@link LogoutResponseConfigurer} * @return the {@link Saml2LogoutConfigurer} for further customizations @@ -227,12 +216,17 @@ public final class Saml2LogoutConfigurer> this.logoutHandlers = logout.getLogoutHandlers(); this.logoutSuccessHandler = logout.getLogoutSuccessHandler(); } - RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http); + RelyingPartyRegistrationResolver registrations = relyingPartyRegistrationResolver(http); http.addFilterBefore(createLogoutRequestProcessingFilter(registrations), CsrfFilter.class); http.addFilterBefore(createLogoutResponseProcessingFilter(registrations), CsrfFilter.class); http.addFilterBefore(createRelyingPartyLogoutFilter(registrations), LogoutFilter.class); } + private RelyingPartyRegistrationResolver relyingPartyRegistrationResolver(H http) { + RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http); + return new DefaultRelyingPartyRegistrationResolver(registrations); + } + private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) { if (this.relyingPartyRegistrationRepository != null) { return this.relyingPartyRegistrationRepository; @@ -248,21 +242,18 @@ public final class Saml2LogoutConfigurer> } private Saml2LogoutRequestFilter createLogoutRequestProcessingFilter( - RelyingPartyRegistrationRepository registrations) { + RelyingPartyRegistrationResolver registrations) { LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]); Saml2LogoutResponseResolver logoutResponseResolver = createSaml2LogoutResponseResolver(registrations); - RequestMatcher requestMatcher = createLogoutRequestMatcher(); - OpenSamlLogoutRequestValidatorParametersResolver parameters = new OpenSamlLogoutRequestValidatorParametersResolver( - registrations); - parameters.setRequestMatcher(requestMatcher); - Saml2LogoutRequestFilter filter = new Saml2LogoutRequestFilter(parameters, + Saml2LogoutRequestFilter filter = new Saml2LogoutRequestFilter(registrations, this.logoutRequestConfigurer.logoutRequestValidator(), logoutResponseResolver, logoutHandlers); + filter.setLogoutRequestMatcher(createLogoutRequestMatcher()); filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy()); return postProcess(filter); } private Saml2LogoutResponseFilter createLogoutResponseProcessingFilter( - RelyingPartyRegistrationRepository registrations) { + RelyingPartyRegistrationResolver registrations) { Saml2LogoutResponseFilter logoutResponseFilter = new Saml2LogoutResponseFilter(registrations, this.logoutResponseConfigurer.logoutResponseValidator(), this.logoutSuccessHandler); logoutResponseFilter.setLogoutRequestMatcher(createLogoutResponseMatcher()); @@ -270,7 +261,7 @@ public final class Saml2LogoutConfigurer> return postProcess(logoutResponseFilter); } - private LogoutFilter createRelyingPartyLogoutFilter(RelyingPartyRegistrationRepository registrations) { + private LogoutFilter createRelyingPartyLogoutFilter(RelyingPartyRegistrationResolver registrations) { LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]); Saml2RelyingPartyInitiatedLogoutSuccessHandler logoutRequestSuccessHandler = createSaml2LogoutRequestSuccessHandler( registrations); @@ -299,15 +290,15 @@ public final class Saml2LogoutConfigurer> } private Saml2RelyingPartyInitiatedLogoutSuccessHandler createSaml2LogoutRequestSuccessHandler( - RelyingPartyRegistrationRepository registrations) { + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { Saml2LogoutRequestResolver logoutRequestResolver = this.logoutRequestConfigurer - .logoutRequestResolver(registrations); + .logoutRequestResolver(relyingPartyRegistrationResolver); return new Saml2RelyingPartyInitiatedLogoutSuccessHandler(logoutRequestResolver); } private Saml2LogoutResponseResolver createSaml2LogoutResponseResolver( - RelyingPartyRegistrationRepository registrations) { - return this.logoutResponseConfigurer.logoutResponseResolver(registrations); + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + return this.logoutResponseConfigurer.logoutResponseResolver(relyingPartyRegistrationResolver); } private C getBeanOrNull(Class clazz) { @@ -383,14 +374,6 @@ public final class Saml2LogoutConfigurer> return this; } - /** - * @deprecated For removal in 7.0. Use {@link #logoutRequest(Customizer)} or - * {@code logoutRequest(Customizer.withDefaults())} to stick with defaults. See - * the documentation - * for more details. - */ - @Deprecated(since = "6.1", forRemoval = true) public Saml2LogoutConfigurer and() { return Saml2LogoutConfigurer.this; } @@ -402,11 +385,12 @@ public final class Saml2LogoutConfigurer> return this.logoutRequestValidator; } - private Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) { + private Saml2LogoutRequestResolver logoutRequestResolver( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { if (this.logoutRequestResolver != null) { return this.logoutRequestResolver; } - return new OpenSaml4LogoutRequestResolver(registrations); + return new OpenSaml4LogoutRequestResolver(relyingPartyRegistrationResolver); } } @@ -459,14 +443,6 @@ public final class Saml2LogoutConfigurer> return this; } - /** - * @deprecated For removal in 7.0. Use {@link #logoutResponse(Customizer)} or - * {@code logoutResponse(Customizer.withDefaults())} to stick with defaults. See - * the documentation - * for more details. - */ - @Deprecated(since = "6.1", forRemoval = true) public Saml2LogoutConfigurer and() { return Saml2LogoutConfigurer.this; } @@ -478,9 +454,10 @@ public final class Saml2LogoutConfigurer> return this.logoutResponseValidator; } - private Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) { + private Saml2LogoutResponseResolver logoutResponseResolver( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { if (this.logoutResponseResolver == null) { - return new OpenSaml4LogoutResponseResolver(registrations); + return new OpenSaml4LogoutResponseResolver(relyingPartyRegistrationResolver); } return this.logoutResponseResolver; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java deleted file mode 100644 index d64e93a243..0000000000 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.saml2; - -import java.util.function.Function; - -import org.springframework.context.ApplicationContext; -import org.springframework.security.config.annotation.web.HttpSecurityBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver; -import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver; -import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.util.Assert; - -/** - * An {@link AbstractHttpConfigurer} for SAML 2.0 Metadata. - * - *

    - * SAML 2.0 Metadata provides an application with the capability to publish configuration - * information as a {@code } or {@code }. - * - *

    - * Defaults are provided for all configuration options with the only required - * configuration being a {@link Saml2LoginConfigurer#relyingPartyRegistrationRepository}. - * Alternatively, a {@link RelyingPartyRegistrationRepository} {@code @Bean} may be - * registered instead. - * - *

    Security Filters

    - * - * The following {@code Filter} is populated: - * - *
      - *
    • {@link Saml2MetadataFilter}
    • - *
    - * - *

    Shared Objects Created

    - * - * none - * - *

    Shared Objects Used

    - * - * The following shared objects are used: - * - *
      - *
    • {@link RelyingPartyRegistrationRepository} (required)
    • - *
    - * - * @since 6.1 - * @see HttpSecurity#saml2Metadata() - * @see Saml2MetadataFilter - * @see RelyingPartyRegistrationRepository - */ -public class Saml2MetadataConfigurer> - extends AbstractHttpConfigurer, H> { - - private final ApplicationContext context; - - private Function metadataResponseResolver; - - public Saml2MetadataConfigurer(ApplicationContext context) { - this.context = context; - } - - /** - * Use this endpoint to request relying party metadata. - * - *

    - * If you specify a {@code registrationId} placeholder in the URL, then the filter - * will lookup a {@link RelyingPartyRegistration} using that. - * - *

    - * If there is no {@code registrationId} and your - * {@link RelyingPartyRegistrationRepository} is {code Iterable}, the metadata - * endpoint will try and show all relying parties' metadata in a single - * {@code - * If you need a more sophisticated lookup strategy than these, use - * {@link #metadataResponseResolver} instead. - * @param metadataUrl the url to use - * @return the {@link Saml2MetadataConfigurer} for more customizations - */ - public Saml2MetadataConfigurer metadataUrl(String metadataUrl) { - Assert.hasText(metadataUrl, "metadataUrl cannot be empty"); - this.metadataResponseResolver = (registrations) -> { - RequestMatcherMetadataResponseResolver metadata = new RequestMatcherMetadataResponseResolver(registrations, - new OpenSamlMetadataResolver()); - metadata.setRequestMatcher(new AntPathRequestMatcher(metadataUrl)); - return metadata; - }; - return this; - } - - /** - * Use this {@link Saml2MetadataResponseResolver} to parse the request and respond - * with SAML 2.0 metadata. - * @param metadataResponseResolver to use - * @return the {@link Saml2MetadataConfigurer} for more customizations - */ - public Saml2MetadataConfigurer metadataResponseResolver(Saml2MetadataResponseResolver metadataResponseResolver) { - Assert.notNull(metadataResponseResolver, "metadataResponseResolver cannot be null"); - this.metadataResponseResolver = (registrations) -> metadataResponseResolver; - return this; - } - - public H and() { - return getBuilder(); - } - - @Override - public void configure(H http) throws Exception { - Saml2MetadataResponseResolver metadataResponseResolver = createMetadataResponseResolver(http); - http.addFilterBefore(new Saml2MetadataFilter(metadataResponseResolver), BasicAuthenticationFilter.class); - } - - private Saml2MetadataResponseResolver createMetadataResponseResolver(H http) { - if (this.metadataResponseResolver != null) { - RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http); - return this.metadataResponseResolver.apply(registrations); - } - Saml2MetadataResponseResolver metadataResponseResolver = getBeanOrNull(Saml2MetadataResponseResolver.class); - if (metadataResponseResolver != null) { - return metadataResponseResolver; - } - RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http); - return new RequestMatcherMetadataResponseResolver(registrations, new OpenSamlMetadataResolver()); - } - - private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) { - Saml2LoginConfigurer login = http.getConfigurer(Saml2LoginConfigurer.class); - if (login != null) { - return login.relyingPartyRegistrationRepository(http); - } - else { - return getBeanOrNull(RelyingPartyRegistrationRepository.class); - } - } - - private C getBeanOrNull(Class clazz) { - if (this.context == null) { - return null; - } - if (this.context.getBeanNamesForType(clazz).length == 0) { - return null; - } - return this.context.getBean(clazz); - } - -} diff --git a/config/src/main/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessor.java b/config/src/main/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessor.java index 69905dfe5e..7b406b9546 100644 --- a/config/src/main/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessor.java +++ b/config/src/main/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessor.java @@ -68,7 +68,8 @@ public class RsaKeyConversionServicePostProcessor implements BeanFactoryPostProc return; } ConversionService service = beanFactory.getConversionService(); - if (service instanceof ConverterRegistry registry) { + if (service instanceof ConverterRegistry) { + ConverterRegistry registry = (ConverterRegistry) service; registry.addConverter(String.class, RSAPrivateKey.class, this.pkcs8); registry.addConverter(String.class, RSAPublicKey.class, this.x509); } diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index 6a80bafce5..3360f6f64b 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -421,8 +421,6 @@ final class AuthenticationConfigBuilder { this.pc.getReaderContext() .registerWithGeneratedName(new RootBeanDefinition(OAuth2ClientWebMvcSecurityPostProcessor.class)); } - this.pc.getReaderContext() - .registerWithGeneratedName(new RootBeanDefinition(OAuth2AuthorizedClientManagerRegistrar.class)); } private void createSaml2LoginFilter(BeanReference authenticationManager, diff --git a/config/src/main/java/org/springframework/security/config/http/ChannelAttributeFactory.java b/config/src/main/java/org/springframework/security/config/http/ChannelAttributeFactory.java index 0030a34dce..6c00919429 100644 --- a/config/src/main/java/org/springframework/security/config/http/ChannelAttributeFactory.java +++ b/config/src/main/java/org/springframework/security/config/http/ChannelAttributeFactory.java @@ -42,12 +42,19 @@ public final class ChannelAttributeFactory { } public static List createChannelAttributes(String requiredChannel) { - String channelConfigAttribute = switch (requiredChannel) { - case OPT_REQUIRES_HTTPS -> "REQUIRES_SECURE_CHANNEL"; - case OPT_REQUIRES_HTTP -> "REQUIRES_INSECURE_CHANNEL"; - case OPT_ANY_CHANNEL -> ChannelDecisionManagerImpl.ANY_CHANNEL; - default -> throw new BeanCreationException("Unknown channel attribute " + requiredChannel); - }; + String channelConfigAttribute; + if (requiredChannel.equals(OPT_REQUIRES_HTTPS)) { + channelConfigAttribute = "REQUIRES_SECURE_CHANNEL"; + } + else if (requiredChannel.equals(OPT_REQUIRES_HTTP)) { + channelConfigAttribute = "REQUIRES_INSECURE_CHANNEL"; + } + else if (requiredChannel.equals(OPT_ANY_CHANNEL)) { + channelConfigAttribute = ChannelDecisionManagerImpl.ANY_CHANNEL; + } + else { + throw new BeanCreationException("Unknown channel attribute " + requiredChannel); + } return SecurityConfig.createList(channelConfigAttribute); } diff --git a/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java index e8cbad916d..4970bb35a9 100644 --- a/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ import org.springframework.security.web.csrf.CsrfAuthenticationStrategy; import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.security.web.csrf.CsrfLogoutHandler; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; +import org.springframework.security.web.csrf.LazyCsrfTokenRepository; import org.springframework.security.web.csrf.MissingCsrfTokenException; import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor; import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler; @@ -108,12 +109,13 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser { this.requestHandlerRef = element.getAttribute(ATT_REQUEST_HANDLER); } if (!StringUtils.hasText(this.csrfRepositoryRef)) { - BeanDefinitionBuilder httpSessionCsrfTokenRepository = BeanDefinitionBuilder - .rootBeanDefinition(HttpSessionCsrfTokenRepository.class); - this.csrfRepositoryRef = pc.getReaderContext() - .generateBeanName(httpSessionCsrfTokenRepository.getBeanDefinition()); - pc.registerBeanComponent(new BeanComponentDefinition(httpSessionCsrfTokenRepository.getBeanDefinition(), - this.csrfRepositoryRef)); + RootBeanDefinition csrfTokenRepository = new RootBeanDefinition(HttpSessionCsrfTokenRepository.class); + BeanDefinitionBuilder lazyTokenRepository = BeanDefinitionBuilder + .rootBeanDefinition(LazyCsrfTokenRepository.class); + lazyTokenRepository.addConstructorArgValue(csrfTokenRepository); + this.csrfRepositoryRef = pc.getReaderContext().generateBeanName(lazyTokenRepository.getBeanDefinition()); + pc.registerBeanComponent( + new BeanComponentDefinition(lazyTokenRepository.getBeanDefinition(), this.csrfRepositoryRef)); } BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(CsrfFilter.class); builder.addConstructorArgReference(this.csrfRepositoryRef); diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrar.java b/config/src/main/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrar.java deleted file mode 100644 index 8693333c3c..0000000000 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrar.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.http; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.function.Consumer; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.BeanInitializationException; -import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; -import org.springframework.context.annotation.AnnotationBeanNameGenerator; -import org.springframework.core.ResolvableType; -import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; - -/** - * A registrar for registering the default {@link OAuth2AuthorizedClientManager} bean - * definition, if not already present. - *

    - * Note: This class is a direct copy of - * {@link org.springframework.security.config.annotation.web.configuration.OAuth2ClientConfiguration.OAuth2AuthorizedClientManagerRegistrar}. - * - * @author Joe Grandja - * @author Steve Riesenberg - * @since 6.2.0 - */ -final class OAuth2AuthorizedClientManagerRegistrar implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware { - - // @formatter:off - private static final Set> KNOWN_AUTHORIZED_CLIENT_PROVIDERS = Set.of( - AuthorizationCodeOAuth2AuthorizedClientProvider.class, - RefreshTokenOAuth2AuthorizedClientProvider.class, - ClientCredentialsOAuth2AuthorizedClientProvider.class, - PasswordOAuth2AuthorizedClientProvider.class, - JwtBearerOAuth2AuthorizedClientProvider.class - ); - // @formatter:on - - private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator(); - - private ListableBeanFactory beanFactory; - - @Override - public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { - if (getBeanNamesForType(OAuth2AuthorizedClientManager.class).length != 0 - || getBeanNamesForType(ClientRegistrationRepository.class).length != 1 - || getBeanNamesForType(OAuth2AuthorizedClientRepository.class).length != 1) { - return; - } - - BeanDefinition beanDefinition = BeanDefinitionBuilder - .genericBeanDefinition(OAuth2AuthorizedClientManager.class, this::getAuthorizedClientManager) - .getBeanDefinition(); - - registry.registerBeanDefinition(this.beanNameGenerator.generateBeanName(beanDefinition, registry), - beanDefinition); - } - - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = (ListableBeanFactory) beanFactory; - } - - private OAuth2AuthorizedClientManager getAuthorizedClientManager() { - ClientRegistrationRepository clientRegistrationRepository = BeanFactoryUtils - .beanOfTypeIncludingAncestors(this.beanFactory, ClientRegistrationRepository.class, true, true); - - OAuth2AuthorizedClientRepository authorizedClientRepository = BeanFactoryUtils - .beanOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientRepository.class, true, true); - - Collection authorizedClientProviderBeans = BeanFactoryUtils - .beansOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientProvider.class, true, true) - .values(); - - OAuth2AuthorizedClientProvider authorizedClientProvider; - if (hasDelegatingAuthorizedClientProvider(authorizedClientProviderBeans)) { - authorizedClientProvider = authorizedClientProviderBeans.iterator().next(); - } - else { - List authorizedClientProviders = new ArrayList<>(); - authorizedClientProviders.add(getAuthorizationCodeAuthorizedClientProvider(authorizedClientProviderBeans)); - authorizedClientProviders.add(getRefreshTokenAuthorizedClientProvider(authorizedClientProviderBeans)); - authorizedClientProviders.add(getClientCredentialsAuthorizedClientProvider(authorizedClientProviderBeans)); - authorizedClientProviders.add(getPasswordAuthorizedClientProvider(authorizedClientProviderBeans)); - - OAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = getJwtBearerAuthorizedClientProvider( - authorizedClientProviderBeans); - if (jwtBearerAuthorizedClientProvider != null) { - authorizedClientProviders.add(jwtBearerAuthorizedClientProvider); - } - - authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans)); - authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders); - } - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - Consumer authorizedClientManagerConsumer = getBeanOfType( - ResolvableType.forClassWithGenerics(Consumer.class, DefaultOAuth2AuthorizedClientManager.class)); - if (authorizedClientManagerConsumer != null) { - authorizedClientManagerConsumer.accept(authorizedClientManager); - } - - return authorizedClientManager; - } - - private boolean hasDelegatingAuthorizedClientProvider( - Collection authorizedClientProviders) { - if (authorizedClientProviders.size() != 1) { - return false; - } - return authorizedClientProviders.iterator().next() instanceof DelegatingOAuth2AuthorizedClientProvider; - } - - private OAuth2AuthorizedClientProvider getAuthorizationCodeAuthorizedClientProvider( - Collection authorizedClientProviders) { - AuthorizationCodeOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( - authorizedClientProviders, AuthorizationCodeOAuth2AuthorizedClientProvider.class); - if (authorizedClientProvider == null) { - authorizedClientProvider = new AuthorizationCodeOAuth2AuthorizedClientProvider(); - } - - return authorizedClientProvider; - } - - private OAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider( - Collection authorizedClientProviders) { - RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( - authorizedClientProviders, RefreshTokenOAuth2AuthorizedClientProvider.class); - if (authorizedClientProvider == null) { - authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider(); - } - - OAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType( - ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class, - OAuth2RefreshTokenGrantRequest.class)); - if (accessTokenResponseClient != null) { - authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); - } - - return authorizedClientProvider; - } - - private OAuth2AuthorizedClientProvider getClientCredentialsAuthorizedClientProvider( - Collection authorizedClientProviders) { - ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( - authorizedClientProviders, ClientCredentialsOAuth2AuthorizedClientProvider.class); - if (authorizedClientProvider == null) { - authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider(); - } - - OAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType( - ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class, - OAuth2ClientCredentialsGrantRequest.class)); - if (accessTokenResponseClient != null) { - authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); - } - - return authorizedClientProvider; - } - - private OAuth2AuthorizedClientProvider getPasswordAuthorizedClientProvider( - Collection authorizedClientProviders) { - PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( - authorizedClientProviders, PasswordOAuth2AuthorizedClientProvider.class); - if (authorizedClientProvider == null) { - authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider(); - } - - OAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType( - ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class, - OAuth2PasswordGrantRequest.class)); - if (accessTokenResponseClient != null) { - authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); - } - - return authorizedClientProvider; - } - - private OAuth2AuthorizedClientProvider getJwtBearerAuthorizedClientProvider( - Collection authorizedClientProviders) { - JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType( - authorizedClientProviders, JwtBearerOAuth2AuthorizedClientProvider.class); - - OAuth2AccessTokenResponseClient accessTokenResponseClient = getBeanOfType(ResolvableType - .forClassWithGenerics(OAuth2AccessTokenResponseClient.class, JwtBearerGrantRequest.class)); - if (accessTokenResponseClient != null) { - if (authorizedClientProvider == null) { - authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider(); - } - - authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); - } - - return authorizedClientProvider; - } - - private List getAdditionalAuthorizedClientProviders( - Collection authorizedClientProviders) { - List additionalAuthorizedClientProviders = new ArrayList<>( - authorizedClientProviders); - additionalAuthorizedClientProviders - .removeIf((provider) -> KNOWN_AUTHORIZED_CLIENT_PROVIDERS.contains(provider.getClass())); - return additionalAuthorizedClientProviders; - } - - private T getAuthorizedClientProviderByType( - Collection authorizedClientProviders, Class providerClass) { - T authorizedClientProvider = null; - for (OAuth2AuthorizedClientProvider current : authorizedClientProviders) { - if (providerClass.isInstance(current)) { - assertAuthorizedClientProviderIsNull(authorizedClientProvider); - authorizedClientProvider = providerClass.cast(current); - } - } - return authorizedClientProvider; - } - - private static void assertAuthorizedClientProviderIsNull(OAuth2AuthorizedClientProvider authorizedClientProvider) { - if (authorizedClientProvider != null) { - // @formatter:off - throw new BeanInitializationException(String.format( - "Unable to create an %s bean. Expected one bean of type %s, but found multiple. " + - "Please consider defining only a single bean of this type, or define an %s bean yourself.", - OAuth2AuthorizedClientManager.class.getName(), - authorizedClientProvider.getClass().getName(), - OAuth2AuthorizedClientManager.class.getName())); - // @formatter:on - } - } - - private String[] getBeanNamesForType(Class beanClass) { - return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, beanClass, false, false); - } - - private T getBeanOfType(ResolvableType resolvableType) { - ObjectProvider objectProvider = this.beanFactory.getBeanProvider(resolvableType, true); - return objectProvider.getIfAvailable(); - } - -} diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java index df4dc2d586..5c65d8f28e 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java @@ -94,7 +94,7 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser { .rootBeanDefinition(OAuth2AuthorizationRequestRedirectFilter.class); String authorizationRequestResolverRef = (authorizationCodeGrantElt != null) ? authorizationCodeGrantElt.getAttribute(ATT_AUTHORIZATION_REQUEST_RESOLVER_REF) : null; - if (StringUtils.hasLength(authorizationRequestResolverRef)) { + if (!StringUtils.isEmpty(authorizationRequestResolverRef)) { authorizationRequestRedirectFilterBuilder.addConstructorArgReference(authorizationRequestResolverRef); } else { @@ -125,7 +125,7 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser { private BeanMetadataElement getAuthorizationRequestRepository(Element element) { String authorizationRequestRepositoryRef = (element != null) ? element.getAttribute(ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF) : null; - if (StringUtils.hasLength(authorizationRequestRepositoryRef)) { + if (!StringUtils.isEmpty(authorizationRequestRepositoryRef)) { return new RuntimeBeanReference(authorizationRequestRepositoryRef); } return BeanDefinitionBuilder @@ -147,7 +147,7 @@ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser { private BeanMetadataElement getAccessTokenResponseClient(Element element) { String accessTokenResponseClientRef = (element != null) ? element.getAttribute(ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF) : null; - if (StringUtils.hasLength(accessTokenResponseClientRef)) { + if (!StringUtils.isEmpty(accessTokenResponseClientRef)) { return new RuntimeBeanReference(accessTokenResponseClientRef); } return BeanDefinitionBuilder diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserUtils.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserUtils.java index a4566ebd8f..d75d2d2488 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserUtils.java +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserUtils.java @@ -42,7 +42,7 @@ final class OAuth2ClientBeanDefinitionParserUtils { static BeanMetadataElement getClientRegistrationRepository(Element element) { String clientRegistrationRepositoryRef = element.getAttribute(ATT_CLIENT_REGISTRATION_REPOSITORY_REF); - if (StringUtils.hasLength(clientRegistrationRepositoryRef)) { + if (!StringUtils.isEmpty(clientRegistrationRepositoryRef)) { return new RuntimeBeanReference(clientRegistrationRepositoryRef); } return new RuntimeBeanReference(ClientRegistrationRepository.class); @@ -50,7 +50,7 @@ final class OAuth2ClientBeanDefinitionParserUtils { static BeanMetadataElement getAuthorizedClientRepository(Element element) { String authorizedClientRepositoryRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_REPOSITORY_REF); - if (StringUtils.hasLength(authorizedClientRepositoryRef)) { + if (!StringUtils.isEmpty(authorizedClientRepositoryRef)) { return new RuntimeBeanReference(authorizedClientRepositoryRef); } return null; @@ -58,7 +58,7 @@ final class OAuth2ClientBeanDefinitionParserUtils { static BeanMetadataElement getAuthorizedClientService(Element element) { String authorizedClientServiceRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_SERVICE_REF); - if (StringUtils.hasLength(authorizedClientServiceRef)) { + if (!StringUtils.isEmpty(authorizedClientServiceRef)) { return new RuntimeBeanReference(authorizedClientServiceRef); } return null; diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java index 48a6151346..19969d9426 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java @@ -176,7 +176,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { } Object source = parserContext.extractSource(element); String loginProcessingUrl = element.getAttribute(ATT_LOGIN_PROCESSING_URL); - if (StringUtils.hasLength(loginProcessingUrl)) { + if (!StringUtils.isEmpty(loginProcessingUrl)) { WebConfigUtils.validateHttpRedirect(loginProcessingUrl, parserContext, source); oauth2LoginAuthenticationFilterBuilder.addConstructorArgValue(loginProcessingUrl); } @@ -189,7 +189,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { .addConstructorArgValue(accessTokenResponseClient) .addConstructorArgValue(oauth2UserService); String userAuthoritiesMapperRef = element.getAttribute(ATT_USER_AUTHORITIES_MAPPER_REF); - if (StringUtils.hasLength(userAuthoritiesMapperRef)) { + if (!StringUtils.isEmpty(userAuthoritiesMapperRef)) { oauth2LoginAuthenticationProviderBuilder.addPropertyReference("authoritiesMapper", userAuthoritiesMapperRef); } @@ -199,7 +199,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { BeanDefinitionBuilder oauth2AuthorizationRequestRedirectFilterBuilder = BeanDefinitionBuilder .rootBeanDefinition(OAuth2AuthorizationRequestRedirectFilter.class); String authorizationRequestResolverRef = element.getAttribute(ATT_AUTHORIZATION_REQUEST_RESOLVER_REF); - if (StringUtils.hasLength(authorizationRequestResolverRef)) { + if (!StringUtils.isEmpty(authorizationRequestResolverRef)) { oauth2AuthorizationRequestRedirectFilterBuilder.addConstructorArgReference(authorizationRequestResolverRef); } else { @@ -212,7 +212,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { this.oauth2AuthorizationRequestRedirectFilter = oauth2AuthorizationRequestRedirectFilterBuilder .getBeanDefinition(); String authenticationSuccessHandlerRef = element.getAttribute(ATT_AUTHENTICATION_SUCCESS_HANDLER_REF); - if (StringUtils.hasLength(authenticationSuccessHandlerRef)) { + if (!StringUtils.isEmpty(authenticationSuccessHandlerRef)) { oauth2LoginAuthenticationFilterBuilder.addPropertyReference("authenticationSuccessHandler", authenticationSuccessHandlerRef); } @@ -225,7 +225,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { successHandlerBuilder.getBeanDefinition()); } String loginPage = element.getAttribute(ATT_LOGIN_PAGE); - if (StringUtils.hasLength(loginPage)) { + if (!StringUtils.isEmpty(loginPage)) { WebConfigUtils.validateHttpRedirect(loginPage, parserContext, source); this.oauth2LoginAuthenticationEntryPoint = BeanDefinitionBuilder .rootBeanDefinition(LoginUrlAuthenticationEntryPoint.class) @@ -245,7 +245,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { } } String authenticationFailureHandlerRef = element.getAttribute(ATT_AUTHENTICATION_FAILURE_HANDLER_REF); - if (StringUtils.hasLength(authenticationFailureHandlerRef)) { + if (!StringUtils.isEmpty(authenticationFailureHandlerRef)) { oauth2LoginAuthenticationFilterBuilder.addPropertyReference("authenticationFailureHandler", authenticationFailureHandlerRef); } @@ -269,7 +269,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { private BeanMetadataElement getAuthorizationRequestRepository(Element element) { String authorizationRequestRepositoryRef = element.getAttribute(ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF); - if (StringUtils.hasLength(authorizationRequestRepositoryRef)) { + if (!StringUtils.isEmpty(authorizationRequestRepositoryRef)) { return new RuntimeBeanReference(authorizationRequestRepositoryRef); } return BeanDefinitionBuilder @@ -299,11 +299,11 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { "org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider") .addConstructorArgValue(accessTokenResponseClient) .addConstructorArgValue(oidcUserService); - if (StringUtils.hasLength(userAuthoritiesMapperRef)) { + if (!StringUtils.isEmpty(userAuthoritiesMapperRef)) { oidcAuthProviderBuilder.addPropertyReference("authoritiesMapper", userAuthoritiesMapperRef); } String jwtDecoderFactoryRef = element.getAttribute(ATT_JWT_DECODER_FACTORY_REF); - if (StringUtils.hasLength(jwtDecoderFactoryRef)) { + if (!StringUtils.isEmpty(jwtDecoderFactoryRef)) { oidcAuthProviderBuilder.addPropertyReference("jwtDecoderFactory", jwtDecoderFactoryRef); } return oidcAuthProviderBuilder.getBeanDefinition(); @@ -311,7 +311,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { private BeanMetadataElement getOidcUserService(Element element) { String oidcUserServiceRef = element.getAttribute(ATT_OIDC_USER_SERVICE_REF); - if (StringUtils.hasLength(oidcUserServiceRef)) { + if (!StringUtils.isEmpty(oidcUserServiceRef)) { return new RuntimeBeanReference(oidcUserServiceRef); } return BeanDefinitionBuilder @@ -321,7 +321,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { private BeanMetadataElement getOAuth2UserService(Element element) { String oauth2UserServiceRef = element.getAttribute(ATT_USER_SERVICE_REF); - if (StringUtils.hasLength(oauth2UserServiceRef)) { + if (!StringUtils.isEmpty(oauth2UserServiceRef)) { return new RuntimeBeanReference(oauth2UserServiceRef); } return BeanDefinitionBuilder @@ -331,7 +331,7 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { private BeanMetadataElement getAccessTokenResponseClient(Element element) { String accessTokenResponseClientRef = element.getAttribute(ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF); - if (StringUtils.hasLength(accessTokenResponseClientRef)) { + if (!StringUtils.isEmpty(accessTokenResponseClientRef)) { return new RuntimeBeanReference(accessTokenResponseClientRef); } return BeanDefinitionBuilder diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java index 58b896c32b..8c8518bb7e 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java @@ -166,7 +166,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa BeanMetadataElement getAuthenticationManagerResolver(Element element) { String authenticationManagerResolverRef = element.getAttribute(AUTHENTICATION_MANAGER_RESOLVER_REF); - if (StringUtils.hasLength(authenticationManagerResolverRef)) { + if (!StringUtils.isEmpty(authenticationManagerResolverRef)) { return new RuntimeBeanReference(authenticationManagerResolverRef); } BeanDefinitionBuilder authenticationManagerResolver = BeanDefinitionBuilder @@ -177,7 +177,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa BeanMetadataElement getBearerTokenResolver(Element element) { String bearerTokenResolverRef = element.getAttribute(BEARER_TOKEN_RESOLVER_REF); - if (!StringUtils.hasLength(bearerTokenResolverRef)) { + if (StringUtils.isEmpty(bearerTokenResolverRef)) { return new RootBeanDefinition(DefaultBearerTokenResolver.class); } return new RuntimeBeanReference(bearerTokenResolverRef); @@ -185,7 +185,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa BeanMetadataElement getEntryPoint(Element element) { String entryPointRef = element.getAttribute(ENTRY_POINT_REF); - if (!StringUtils.hasLength(entryPointRef)) { + if (StringUtils.isEmpty(entryPointRef)) { return this.authenticationEntryPoint; } return new RuntimeBeanReference(entryPointRef); @@ -224,7 +224,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa Object getDecoder(Element element) { String decoderRef = element.getAttribute(DECODER_REF); - if (StringUtils.hasLength(decoderRef)) { + if (!StringUtils.isEmpty(decoderRef)) { return new RuntimeBeanReference(decoderRef); } BeanDefinitionBuilder builder = BeanDefinitionBuilder @@ -235,7 +235,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa Object getJwtAuthenticationConverter(Element element) { String jwtDecoderRef = element.getAttribute(JWT_AUTHENTICATION_CONVERTER_REF); - return (StringUtils.hasLength(jwtDecoderRef)) ? new RuntimeBeanReference(jwtDecoderRef) + return (!StringUtils.isEmpty(jwtDecoderRef)) ? new RuntimeBeanReference(jwtDecoderRef) : new JwtAuthenticationConverter(); } @@ -293,7 +293,7 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa BeanMetadataElement getIntrospector(Element element) { String introspectorRef = element.getAttribute(INTROSPECTOR_REF); - if (StringUtils.hasLength(introspectorRef)) { + if (!StringUtils.isEmpty(introspectorRef)) { return new RuntimeBeanReference(introspectorRef); } String introspectionUri = element.getAttribute(INTROSPECTION_URI); diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java index 80ff475579..91a308f68b 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java @@ -174,7 +174,7 @@ public final class ClientRegistrationsBeanDefinitionParser implements BeanDefini if (providers.containsKey(providerId)) { Map provider = providers.get(providerId); String issuer = provider.get(ATT_ISSUER_URI); - if (StringUtils.hasLength(issuer)) { + if (!StringUtils.isEmpty(issuer)) { ClientRegistration.Builder builder = ClientRegistrations.fromIssuerLocation(issuer) .registrationId(registrationId); return getBuilder(parserContext, builder, provider); diff --git a/config/src/main/java/org/springframework/security/config/web/server/DefaultOidcLogoutTokenValidatorFactory.java b/config/src/main/java/org/springframework/security/config/web/server/DefaultOidcLogoutTokenValidatorFactory.java deleted file mode 100644 index 22c26aa8d7..0000000000 --- a/config/src/main/java/org/springframework/security/config/web/server/DefaultOidcLogoutTokenValidatorFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server; - -import java.util.function.Function; - -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtTimestampValidator; - -final class DefaultOidcLogoutTokenValidatorFactory implements Function> { - - @Override - public OAuth2TokenValidator apply(ClientRegistration clientRegistration) { - return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(), - new OidcBackChannelLogoutTokenValidator(clientRegistration)); - } - -} diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutAuthentication.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutAuthentication.java deleted file mode 100644 index c68063b614..0000000000 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutAuthentication.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server; - -import java.util.Collections; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; - -/** - * An {@link org.springframework.security.core.Authentication} implementation that - * represents the result of authenticating an OIDC Logout token for the purposes of - * performing Back-Channel Logout. - * - * @author Josh Cummings - * @since 6.2 - * @see OidcLogoutAuthenticationToken - * @see OIDC Back-Channel - * Logout - */ -class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken { - - private final OidcLogoutToken logoutToken; - - /** - * Construct an {@link OidcBackChannelLogoutAuthentication} - * @param logoutToken a deserialized, verified OIDC Logout Token - */ - OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) { - super(Collections.emptyList()); - this.logoutToken = logoutToken; - setAuthenticated(true); - } - - /** - * {@inheritDoc} - */ - @Override - public OidcLogoutToken getPrincipal() { - return this.logoutToken; - } - - /** - * {@inheritDoc} - */ - @Override - public OidcLogoutToken getCredentials() { - return this.logoutToken; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java deleted file mode 100644 index 1cd87fc830..0000000000 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server; - -import reactor.core.publisher.Mono; - -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.AuthenticationServiceException; -import org.springframework.security.authentication.ReactiveAuthenticationManager; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.client.oidc.authentication.ReactiveOidcIdTokenDecoderFactory; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.jwt.BadJwtException; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtDecoderFactory; -import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; -import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory; -import org.springframework.util.Assert; - -/** - * An {@link AuthenticationProvider} that authenticates an OIDC Logout Token; namely - * deserializing it, verifying its signature, and validating its claims. - * - *

    - * Intended to be included in a - * {@link org.springframework.security.authentication.ProviderManager} - * - * @author Josh Cummings - * @since 6.2 - * @see OidcLogoutAuthenticationToken - * @see org.springframework.security.authentication.ProviderManager - * @see OIDC Back-Channel - * Logout - */ -final class OidcBackChannelLogoutReactiveAuthenticationManager implements ReactiveAuthenticationManager { - - private ReactiveJwtDecoderFactory logoutTokenDecoderFactory; - - /** - * Construct an {@link OidcBackChannelLogoutReactiveAuthenticationManager} - */ - OidcBackChannelLogoutReactiveAuthenticationManager() { - ReactiveOidcIdTokenDecoderFactory logoutTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory(); - logoutTokenDecoderFactory.setJwtValidatorFactory(new DefaultOidcLogoutTokenValidatorFactory()); - this.logoutTokenDecoderFactory = logoutTokenDecoderFactory; - } - - /** - * {@inheritDoc} - */ - @Override - public Mono authenticate(Authentication authentication) throws AuthenticationException { - if (!(authentication instanceof OidcLogoutAuthenticationToken token)) { - return Mono.empty(); - } - String logoutToken = token.getLogoutToken(); - ClientRegistration registration = token.getClientRegistration(); - return decode(registration, logoutToken) - .map((jwt) -> OidcLogoutToken.withTokenValue(logoutToken) - .claims((claims) -> claims.putAll(jwt.getClaims())) - .build()) - .map(OidcBackChannelLogoutAuthentication::new); - } - - private Mono decode(ClientRegistration registration, String token) { - ReactiveJwtDecoder logoutTokenDecoder = this.logoutTokenDecoderFactory.createDecoder(registration); - try { - return logoutTokenDecoder.decode(token); - } - catch (BadJwtException failed) { - OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, failed.getMessage(), - "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation"); - return Mono.error(new OAuth2AuthenticationException(error, failed)); - } - catch (Exception failed) { - return Mono.error(new AuthenticationServiceException(failed.getMessage(), failed)); - } - } - - /** - * Use this {@link ReactiveJwtDecoderFactory} to generate {@link JwtDecoder}s that - * correspond to the {@link ClientRegistration} associated with the OIDC logout token. - * @param logoutTokenDecoderFactory the {@link JwtDecoderFactory} to use - */ - void setLogoutTokenDecoderFactory(ReactiveJwtDecoderFactory logoutTokenDecoderFactory) { - Assert.notNull(logoutTokenDecoderFactory, "logoutTokenDecoderFactory cannot be null"); - this.logoutTokenDecoderFactory = logoutTokenDecoderFactory; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.java deleted file mode 100644 index 7053689171..0000000000 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimAccessor; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; -import org.springframework.security.oauth2.jwt.Jwt; - -/** - * A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance - * with the OIDC Back-Channel Logout Spec. - * - * @author Josh Cummings - * @since 6.2 - * @see OidcLogoutToken - * @see Logout - * Token - * @see the OIDC - * Back-Channel Logout spec - */ -final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator { - - private static final String LOGOUT_VALIDATION_URL = "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation"; - - private static final String BACK_CHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout"; - - private final String audience; - - private final String issuer; - - OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) { - this.audience = clientRegistration.getClientId(); - this.issuer = clientRegistration.getProviderDetails().getIssuerUri(); - } - - @Override - public OAuth2TokenValidatorResult validate(Jwt jwt) { - Collection errors = new ArrayList<>(); - - LogoutTokenClaimAccessor logoutClaims = jwt::getClaims; - Map events = logoutClaims.getEvents(); - if (events == null) { - errors.add(invalidLogoutToken("events claim must not be null")); - } - else if (events.get(BACK_CHANNEL_LOGOUT_EVENT) == null) { - errors.add(invalidLogoutToken("events claim map must contain \"" + BACK_CHANNEL_LOGOUT_EVENT + "\" key")); - } - - String issuer = logoutClaims.getIssuer().toExternalForm(); - if (issuer == null) { - errors.add(invalidLogoutToken("iss claim must not be null")); - } - else if (!this.issuer.equals(issuer)) { - errors.add(invalidLogoutToken( - "iss claim value must match `ClientRegistration#getProviderDetails#getIssuerUri`")); - } - - List audience = logoutClaims.getAudience(); - if (audience == null) { - errors.add(invalidLogoutToken("aud claim must not be null")); - } - else if (!audience.contains(this.audience)) { - errors.add(invalidLogoutToken("aud claim value must include `ClientRegistration#getClientId`")); - } - - Instant issuedAt = logoutClaims.getIssuedAt(); - if (issuedAt == null) { - errors.add(invalidLogoutToken("iat claim must not be null")); - } - - String jwtId = logoutClaims.getId(); - if (jwtId == null) { - errors.add(invalidLogoutToken("jti claim must not be null")); - } - - if (logoutClaims.getSubject() == null && logoutClaims.getSessionId() == null) { - errors.add(invalidLogoutToken("sub and sid claims must not both be null")); - } - - if (logoutClaims.getClaim("nonce") != null) { - errors.add(invalidLogoutToken("nonce claim must not be present")); - } - - return OAuth2TokenValidatorResult.failure(errors); - } - - private static OAuth2Error invalidLogoutToken(String description) { - return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, description, LOGOUT_VALIDATION_URL); - } - -} diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutWebFilter.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutWebFilter.java deleted file mode 100644 index 74f5f32e68..0000000000 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutWebFilter.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server; - -import java.nio.charset.StandardCharsets; - -import jakarta.servlet.http.HttpServletResponse; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationServiceException; -import org.springframework.security.authentication.ReactiveAuthenticationManager; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.web.authentication.AuthenticationConverter; -import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.security.web.server.WebFilterExchange; -import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; -import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; -import org.springframework.util.Assert; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilter; -import org.springframework.web.server.WebFilterChain; - -/** - * A filter for the Client-side OIDC Back-Channel Logout endpoint - * - * @author Josh Cummings - * @since 6.2 - * @see OIDC Back-Channel Logout - * Spec - */ -class OidcBackChannelLogoutWebFilter implements WebFilter { - - private final Log logger = LogFactory.getLog(getClass()); - - private final ServerAuthenticationConverter authenticationConverter; - - private final ReactiveAuthenticationManager authenticationManager; - - private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(); - - /** - * Construct an {@link OidcBackChannelLogoutWebFilter} - * @param authenticationConverter the {@link AuthenticationConverter} for deriving - * Logout Token authentication - * @param authenticationManager the {@link AuthenticationManager} for authenticating - * Logout Tokens - */ - OidcBackChannelLogoutWebFilter(ServerAuthenticationConverter authenticationConverter, - ReactiveAuthenticationManager authenticationManager) { - Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); - Assert.notNull(authenticationManager, "authenticationManager cannot be null"); - this.authenticationConverter = authenticationConverter; - this.authenticationManager = authenticationManager; - } - - @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - return this.authenticationConverter.convert(exchange).onErrorResume(AuthenticationException.class, (ex) -> { - this.logger.debug("Failed to process OIDC Back-Channel Logout", ex); - if (ex instanceof AuthenticationServiceException) { - return Mono.error(ex); - } - return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty()); - }) - .switchIfEmpty(chain.filter(exchange).then(Mono.empty())) - .flatMap(this.authenticationManager::authenticate) - .onErrorResume(AuthenticationException.class, (ex) -> { - this.logger.debug("Failed to process OIDC Back-Channel Logout", ex); - if (ex instanceof AuthenticationServiceException) { - return Mono.error(ex); - } - return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty()); - }) - .flatMap((authentication) -> { - WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain); - return this.logoutHandler.logout(webFilterExchange, authentication); - }); - } - - private Mono handleAuthenticationFailure(ServerHttpResponse response, Exception ex) { - this.logger.debug("Failed to process OIDC Back-Channel Logout", ex); - response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST); - OAuth2Error error = oauth2Error(ex); - byte[] bytes = String.format(""" - { - "error_code": "%s", - "error_description": "%s", - "error_uri: "%s" - } - """, error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8); - DataBuffer buffer = response.bufferFactory().wrap(bytes); - return response.writeWith(Flux.just(buffer)); - } - - private OAuth2Error oauth2Error(Exception ex) { - if (ex instanceof OAuth2AuthenticationException oauth2) { - return oauth2.getError(); - } - return new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, ex.getMessage(), - "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation"); - } - - /** - * The strategy for expiring all Client sessions indicated by the logout request. - * Defaults to {@link OidcBackChannelServerLogoutHandler}. - * @param logoutHandler the {@link LogoutHandler} to use - */ - void setLogoutHandler(ServerLogoutHandler logoutHandler) { - Assert.notNull(logoutHandler, "logoutHandler cannot be null"); - this.logoutHandler = logoutHandler; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandler.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandler.java deleted file mode 100644 index 9cb49de176..0000000000 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelServerLogoutHandler.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server; - -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -import jakarta.servlet.http.HttpServletResponse; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry; -import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.web.server.WebFilterExchange; -import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; -import org.springframework.util.Assert; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.util.UriComponentsBuilder; - -/** - * A {@link ServerLogoutHandler} that locates the sessions associated with a given OIDC - * Back-Channel Logout Token and invalidates each one. - * - * @author Josh Cummings - * @since 6.2 - * @see OIDC Back-Channel Logout - * Spec - */ -final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler { - - private final Log logger = LogFactory.getLog(getClass()); - - private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry(); - - private WebClient web = WebClient.create(); - - private String logoutEndpointName = "/logout"; - - private String sessionCookieName = "SESSION"; - - @Override - public Mono logout(WebFilterExchange exchange, Authentication authentication) { - if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) { - return Mono.defer(() -> { - if (this.logger.isDebugEnabled()) { - String message = "Did not perform OIDC Back-Channel Logout since authentication [%s] was of the wrong type"; - this.logger.debug(String.format(message, authentication.getClass().getSimpleName())); - } - return Mono.empty(); - }); - } - AtomicInteger totalCount = new AtomicInteger(0); - AtomicInteger invalidatedCount = new AtomicInteger(0); - return this.sessionRegistry.removeSessionInformation(token.getPrincipal()).concatMap((session) -> { - totalCount.incrementAndGet(); - return eachLogout(exchange, session).flatMap((response) -> { - invalidatedCount.incrementAndGet(); - return Mono.empty(); - }).onErrorResume((ex) -> { - this.logger.debug("Failed to invalidate session", ex); - return this.sessionRegistry.saveSessionInformation(session).then(Mono.just(ex.getMessage())); - }); - }).collectList().flatMap((list) -> { - if (this.logger.isTraceEnabled()) { - this.logger.trace(String.format("Invalidated %d out of %d sessions", invalidatedCount.intValue(), - totalCount.intValue())); - } - if (!list.isEmpty()) { - return handleLogoutFailure(exchange.getExchange().getResponse(), oauth2Error(list)); - } - else { - return Mono.empty(); - } - }); - } - - private Mono> eachLogout(WebFilterExchange exchange, OidcSessionInformation session) { - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId()); - for (Map.Entry credential : session.getAuthorities().entrySet()) { - headers.add(credential.getKey(), credential.getValue()); - } - String url = exchange.getExchange().getRequest().getURI().toString(); - String logout = UriComponentsBuilder.fromHttpUrl(url) - .replacePath(this.logoutEndpointName) - .build() - .toUriString(); - return this.web.post().uri(logout).headers((h) -> h.putAll(headers)).retrieve().toBodilessEntity(); - } - - private OAuth2Error oauth2Error(Collection errors) { - return new OAuth2Error("partial_logout", "not all sessions were terminated: " + errors, - "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation"); - } - - private Mono handleLogoutFailure(ServerHttpResponse response, OAuth2Error error) { - response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST); - byte[] bytes = String.format(""" - { - "error_code": "%s", - "error_description": "%s", - "error_uri: "%s" - } - """, error.getErrorCode(), error.getDescription(), error.getUri()).getBytes(StandardCharsets.UTF_8); - DataBuffer buffer = response.bufferFactory().wrap(bytes); - return response.writeWith(Flux.just(buffer)); - } - - /** - * Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that - * this class uses - * {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify - * sessions. - * @param sessionRegistry the {@link OidcSessionRegistry} to use - */ - void setSessionRegistry(ReactiveOidcSessionRegistry sessionRegistry) { - Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); - this.sessionRegistry = sessionRegistry; - } - - /** - * Use this {@link WebClient} to perform the per-session back-channel logout - * @param web the {@link WebClient} to use - */ - void setWebClient(WebClient web) { - Assert.notNull(web, "web cannot be null"); - this.web = web; - } - - /** - * Use this logout URI for performing per-session logout. Defaults to {@code /logout} - * since that is the default URI for - * {@link org.springframework.security.web.authentication.logout.LogoutFilter}. - * @param logoutUri the URI to use - */ - void setLogoutUri(String logoutUri) { - Assert.hasText(logoutUri, "logoutUri cannot be empty"); - this.logoutEndpointName = logoutUri; - } - - /** - * Use this cookie name for the session identifier. Defaults to {@code JSESSIONID}. - * - *

    - * Note that if you are using Spring Session, this likely needs to change to SESSION. - * @param sessionCookieName the cookie name to use - */ - void setSessionCookieName(String sessionCookieName) { - Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty"); - this.sessionCookieName = sessionCookieName; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcLogoutAuthenticationToken.java b/config/src/main/java/org/springframework/security/config/web/server/OidcLogoutAuthenticationToken.java deleted file mode 100644 index 8d5ab818a5..0000000000 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcLogoutAuthenticationToken.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.oauth2.client.registration.ClientRegistration; - -/** - * An {@link org.springframework.security.core.Authentication} instance that represents a - * request to authenticate an OIDC Logout Token. - * - * @author Josh Cummings - * @since 6.2 - */ -class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken { - - private final String logoutToken; - - private final ClientRegistration clientRegistration; - - /** - * Construct an {@link OidcLogoutAuthenticationToken} - * @param logoutToken a signed, serialized OIDC Logout token - * @param clientRegistration the {@link ClientRegistration client} associated with - * this token; this is usually derived from material in the logout HTTP request - */ - OidcLogoutAuthenticationToken(String logoutToken, ClientRegistration clientRegistration) { - super(AuthorityUtils.NO_AUTHORITIES); - this.logoutToken = logoutToken; - this.clientRegistration = clientRegistration; - } - - /** - * {@inheritDoc} - */ - @Override - public String getCredentials() { - return this.logoutToken; - } - - /** - * {@inheritDoc} - */ - @Override - public String getPrincipal() { - return this.logoutToken; - } - - /** - * Get the signed, serialized OIDC Logout token - * @return the logout token - */ - String getLogoutToken() { - return this.logoutToken; - } - - /** - * Get the {@link ClientRegistration} associated with this logout token - * @return the {@link ClientRegistration} - */ - ClientRegistration getClientRegistration() { - return this.clientRegistration; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcLogoutServerAuthenticationConverter.java b/config/src/main/java/org/springframework/security/config/web/server/OidcLogoutServerAuthenticationConverter.java deleted file mode 100644 index 02bdeb3472..0000000000 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcLogoutServerAuthenticationConverter.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Mono; - -import org.springframework.http.HttpMethod; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.web.authentication.AuthenticationConverter; -import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; -import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; -import org.springframework.util.Assert; -import org.springframework.web.server.ServerWebExchange; - -/** - * An {@link AuthenticationConverter} that extracts the OIDC Logout Token authentication - * request - * - * @author Josh Cummings - * @since 6.2 - */ -final class OidcLogoutServerAuthenticationConverter implements ServerAuthenticationConverter { - - private static final String DEFAULT_LOGOUT_URI = "/logout/connect/back-channel/{registrationId}"; - - private final Log logger = LogFactory.getLog(getClass()); - - private final ReactiveClientRegistrationRepository clientRegistrationRepository; - - private ServerWebExchangeMatcher exchangeMatcher = new PathPatternParserServerWebExchangeMatcher(DEFAULT_LOGOUT_URI, - HttpMethod.POST); - - OidcLogoutServerAuthenticationConverter(ReactiveClientRegistrationRepository clientRegistrationRepository) { - Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); - this.clientRegistrationRepository = clientRegistrationRepository; - } - - @Override - public Mono convert(ServerWebExchange exchange) { - return this.exchangeMatcher.matches(exchange) - .filter(ServerWebExchangeMatcher.MatchResult::isMatch) - .flatMap((match) -> { - String registrationId = (String) match.getVariables().get("registrationId"); - return this.clientRegistrationRepository.findByRegistrationId(registrationId) - .switchIfEmpty(Mono.error(() -> { - this.logger - .debug("Did not process OIDC Back-Channel Logout since no ClientRegistration was found"); - return new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); - })); - }) - .flatMap((clientRegistration) -> exchange.getFormData().map((data) -> { - String logoutToken = data.getFirst("logout_token"); - return new OidcLogoutAuthenticationToken(logoutToken, clientRegistration); - }).switchIfEmpty(Mono.error(() -> { - this.logger.debug("Failed to process OIDC Back-Channel Logout since no logout token was found"); - return new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); - }))); - } - - /** - * The logout endpoint. Defaults to - * {@code /logout/connect/back-channel/{registrationId}}. - * @param exchangeMatcher the {@link ServerWebExchangeMatcher} to use - */ - void setExchangeMatcher(ServerWebExchangeMatcher exchangeMatcher) { - Assert.notNull(exchangeMatcher, "exchangeMatcher cannot be null"); - this.exchangeMatcher = exchangeMatcher; - } - -} diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 3df5775afc..8bdb4553f6 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -21,7 +21,6 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.security.interfaces.RSAPublicKey; import java.time.Duration; -import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -29,13 +28,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import io.micrometer.observation.ObservationRegistry; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; import reactor.util.context.Context; @@ -71,9 +67,6 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCo import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.WebClientReactiveAuthorizationCodeTokenResponseClient; import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager; -import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry; -import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -120,7 +113,6 @@ 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.ServerRedirectStrategy; -import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter; import org.springframework.security.web.server.authentication.AuthenticationConverterServerWebExchangeMatcher; import org.springframework.security.web.server.authentication.AuthenticationWebFilter; @@ -155,7 +147,6 @@ import org.springframework.security.web.server.context.SecurityContextServerWebE import org.springframework.security.web.server.context.ServerSecurityContextRepository; import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; import org.springframework.security.web.server.csrf.CsrfServerLogoutHandler; -import org.springframework.security.web.server.csrf.CsrfToken; import org.springframework.security.web.server.csrf.CsrfWebFilter; import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository; import org.springframework.security.web.server.csrf.ServerCsrfTokenRequestHandler; @@ -202,10 +193,8 @@ import org.springframework.web.cors.reactive.CorsWebFilter; import org.springframework.web.cors.reactive.DefaultCorsProcessor; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.ServerWebExchangeDecorator; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; -import org.springframework.web.server.WebSession; import org.springframework.web.util.pattern.PathPatternParser; /** @@ -306,8 +295,6 @@ public class ServerHttpSecurity { private OAuth2ClientSpec client; - private OidcLogoutSpec oidcLogout; - private LogoutSpec logout = new LogoutSpec(); private LoginPageSpec loginPage = new LoginPageSpec(); @@ -436,13 +423,7 @@ public class ServerHttpSecurity { * } * * @return the {@link HttpsRedirectSpec} to customize - * @deprecated For removal in 7.0. Use {@link #redirectToHttps(Customizer)} or - * {@code redirectToHttps(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public HttpsRedirectSpec redirectToHttps() { this.httpsRedirectSpec = new HttpsRedirectSpec(); return this.httpsRedirectSpec; @@ -523,12 +504,7 @@ public class ServerHttpSecurity { * } * * @return the {@link CsrfSpec} to customize - * @deprecated For removal in 7.0. Use {@link #csrf(Customizer)} or - * {@code csrf(Customizer.withDefaults())} to stick with defaults. See the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public CsrfSpec csrf() { if (this.csrf == null) { this.csrf = new CsrfSpec(); @@ -592,12 +568,7 @@ public class ServerHttpSecurity { * used instead. If neither has been configured, the Cors configuration will do * nothing. * @return the {@link CorsSpec} to customize - * @deprecated For removal in 7.0. Use {@link #cors(Customizer)} or - * {@code cors(Customizer.withDefaults())} to stick with defaults. See the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public CorsSpec cors() { if (this.cors == null) { this.cors = new CorsSpec(); @@ -639,13 +610,7 @@ public class ServerHttpSecurity { * * @return the {@link AnonymousSpec} to customize * @since 5.2.0 - * @deprecated For removal in 7.0. Use {@link #anonymous(Customizer)} or - * {@code anonymous(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public AnonymousSpec anonymous() { if (this.anonymous == null) { this.anonymous = new AnonymousSpec(); @@ -699,13 +664,7 @@ public class ServerHttpSecurity { * } * * @return the {@link HttpBasicSpec} to customize - * @deprecated For removal in 7.0. Use {@link #httpBasic(Customizer)} or - * {@code httpBasic(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public HttpBasicSpec httpBasic() { if (this.httpBasic == null) { this.httpBasic = new HttpBasicSpec(); @@ -757,13 +716,7 @@ public class ServerHttpSecurity { * * @return the {@link PasswordManagementSpec} to customize * @since 5.6 - * @deprecated For removal in 7.0. Use {@link #passwordManagement(Customizer)} or - * {@code passwordManagement(Customizer.withDefaults())} to stick with defaults. See - * the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public PasswordManagementSpec passwordManagement() { if (this.passwordManagement == null) { this.passwordManagement = new PasswordManagementSpec(); @@ -820,13 +773,7 @@ public class ServerHttpSecurity { * } * * @return the {@link FormLoginSpec} to customize - * @deprecated For removal in 7.0. Use {@link #formLogin(Customizer)} or - * {@code formLogin(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public FormLoginSpec formLogin() { if (this.formLogin == null) { this.formLogin = new FormLoginSpec(); @@ -887,12 +834,7 @@ public class ServerHttpSecurity { * {@link ReactivePreAuthenticatedAuthenticationManager} will be used. * @return the {@link X509Spec} to customize * @since 5.2 - * @deprecated For removal in 7.0. Use {@link #x509(Customizer)} or - * {@code x509(Customizer.withDefaults())} to stick with defaults. See the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public X509Spec x509() { if (this.x509 == null) { this.x509 = new X509Spec(); @@ -949,13 +891,7 @@ public class ServerHttpSecurity { * } * * @return the {@link OAuth2LoginSpec} to customize - * @deprecated For removal in 7.0. Use {@link #oauth2Login(Customizer)} or - * {@code oauth2Login(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2LoginSpec oauth2Login() { if (this.oauth2Login == null) { this.oauth2Login = new OAuth2LoginSpec(); @@ -1007,13 +943,7 @@ public class ServerHttpSecurity { * } * * @return the {@link OAuth2ClientSpec} to customize - * @deprecated For removal in 7.0. Use {@link #oauth2Client(Customizer)} or - * {@code oauth2Client(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2ClientSpec oauth2Client() { if (this.client == null) { this.client = new OAuth2ClientSpec(); @@ -1064,10 +994,7 @@ public class ServerHttpSecurity { * } * * @return the {@link OAuth2ResourceServerSpec} to customize - * @deprecated For removal in 7.0. Use {@link #oauth2ResourceServer(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2ResourceServerSpec oauth2ResourceServer() { if (this.resourceServer == null) { this.resourceServer = new OAuth2ResourceServerSpec(); @@ -1106,33 +1033,6 @@ public class ServerHttpSecurity { return this; } - /** - * Configures OIDC Connect 1.0 Logout support. - * - *

    -	 *  @Bean
    -	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    -	 *      http
    -	 *          // ...
    -	 *          .oidcLogout((logout) -> logout
    -	 *              .backChannel(Customizer.withDefaults())
    -	 *          );
    -	 *      return http.build();
    -	 *  }
    -	 * 
    - * @param oidcLogoutCustomizer the {@link Customizer} to provide more options for the - * {@link OidcLogoutSpec} - * @return the {@link ServerHttpSecurity} to customize - * @since 6.2 - */ - public ServerHttpSecurity oidcLogout(Customizer oidcLogoutCustomizer) { - if (this.oidcLogout == null) { - this.oidcLogout = new OidcLogoutSpec(); - } - oidcLogoutCustomizer.customize(this.oidcLogout); - return this; - } - /** * Configures HTTP Response Headers. The default headers are: * @@ -1166,12 +1066,7 @@ public class ServerHttpSecurity { * } * * @return the {@link HeaderSpec} to customize - * @deprecated For removal in 7.0. Use {@link #headers(Customizer)} or - * {@code headers(Customizer.withDefaults())} to stick with defaults. See the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public HeaderSpec headers() { if (this.headers == null) { this.headers = new HeaderSpec(); @@ -1245,13 +1140,7 @@ public class ServerHttpSecurity { * } * * @return the {@link ExceptionHandlingSpec} to customize - * @deprecated For removal in 7.0. Use {@link #exceptionHandling(Customizer)} or - * {@code exceptionHandling(Customizer.withDefaults())} to stick with defaults. See - * the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ExceptionHandlingSpec exceptionHandling() { if (this.exceptionHandling == null) { this.exceptionHandling = new ExceptionHandlingSpec(); @@ -1317,13 +1206,7 @@ public class ServerHttpSecurity { * } * * @return the {@link AuthorizeExchangeSpec} to customize - * @deprecated For removal in 7.0. Use {@link #authorizeExchange(Customizer)} or - * {@code authorizeExchange(Customizer.withDefaults())} to stick with defaults. See - * the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public AuthorizeExchangeSpec authorizeExchange() { if (this.authorizeExchange == null) { this.authorizeExchange = new AuthorizeExchangeSpec(); @@ -1392,12 +1275,7 @@ public class ServerHttpSecurity { * } * * @return the {@link LogoutSpec} to customize - * @deprecated For removal in 7.0. Use {@link #logout(Customizer)} or - * {@code logout(Customizer.withDefaults())} to stick with defaults. See the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public LogoutSpec logout() { if (this.logout == null) { this.logout = new LogoutSpec(); @@ -1454,13 +1332,7 @@ public class ServerHttpSecurity { * } * * @return the {@link RequestCacheSpec} to customize - * @deprecated For removal in 7.0. Use {@link #requestCache(Customizer)} or - * {@code requestCache(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public RequestCacheSpec requestCache() { return this.requestCache; } @@ -1577,9 +1449,6 @@ public class ServerHttpSecurity { if (this.resourceServer != null) { this.resourceServer.configure(this); } - if (this.oidcLogout != null) { - this.oidcLogout.configure(this); - } if (this.client != null) { this.client.configure(this); } @@ -1753,10 +1622,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #authorizeExchange(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -1970,9 +1836,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated use {@link #redirectToHttps(Customizer)} */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -2050,13 +1914,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #csrf(Customizer)} or - * {@code csrf(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -2122,10 +1980,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #exceptionHandling(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -2167,13 +2022,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #requestCache(Customizer)} or - * {@code requestCache(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -2269,13 +2118,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #httpBasic(Customizer)} or - * {@code httpBasic(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -2348,10 +2191,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity}. * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #passwordManagement(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -2509,13 +2349,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #formLogin(Customizer)} or - * {@code formLogin(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -2639,13 +2473,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #headers(Customizer)} or - * {@code headers(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -2662,13 +2490,7 @@ public class ServerHttpSecurity { /** * Configures cache control headers * @return the {@link CacheSpec} to configure - * @deprecated For removal in 7.0. Use {@link #cache(Customizer)} or - * {@code cache(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public CacheSpec cache() { return new CacheSpec(); } @@ -2687,10 +2509,7 @@ public class ServerHttpSecurity { /** * Configures content type response headers * @return the {@link ContentTypeOptionsSpec} to configure - * @deprecated For removal in 7.0. Use {@link #contentTypeOptions(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public ContentTypeOptionsSpec contentTypeOptions() { return new ContentTypeOptionsSpec(); } @@ -2709,13 +2528,7 @@ public class ServerHttpSecurity { /** * Configures frame options response headers * @return the {@link FrameOptionsSpec} to configure - * @deprecated For removal in 7.0. Use {@link #frameOptions(Customizer)} or - * {@code frameOptions(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public FrameOptionsSpec frameOptions() { return new FrameOptionsSpec(); } @@ -2747,13 +2560,7 @@ public class ServerHttpSecurity { /** * Configures the Strict Transport Security response headers * @return the {@link HstsSpec} to configure - * @deprecated For removal in 7.0. Use {@link #hsts(Customizer)} or - * {@code hsts(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public HstsSpec hsts() { return new HstsSpec(); } @@ -2778,13 +2585,7 @@ public class ServerHttpSecurity { /** * Configures x-xss-protection response header. * @return the {@link XssProtectionSpec} to configure - * @deprecated For removal in 7.0. Use {@link #xssProtection(Customizer)} or - * {@code xssProtection(Customizer.withDefaults())} to stick with defaults. See - * the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public XssProtectionSpec xssProtection() { return new XssProtectionSpec(); } @@ -2804,10 +2605,7 @@ public class ServerHttpSecurity { * Configures {@code Content-Security-Policy} response header. * @param policyDirectives the policy directive(s) * @return the {@link ContentSecurityPolicySpec} to configure - * @deprecated For removal in 7.0. Use {@link #contentSecurityPolicy(Customizer)} - * instead. */ - @Deprecated(since = "6.1", forRemoval = true) public ContentSecurityPolicySpec contentSecurityPolicy(String policyDirectives) { return new ContentSecurityPolicySpec(policyDirectives); } @@ -2827,8 +2625,7 @@ public class ServerHttpSecurity { * Configures {@code Feature-Policy} response header. * @param policyDirectives the policy * @return the {@link FeaturePolicySpec} to configure - * @deprecated For removal in 7.0. Use {@link #permissionsPolicy(Customizer)} - * instead. + * @deprecated Use {@link #permissionsPolicy(Customizer)} instead. */ @Deprecated public FeaturePolicySpec featurePolicy(String policyDirectives) { @@ -2838,10 +2635,7 @@ public class ServerHttpSecurity { /** * Configures {@code Permissions-Policy} response header. * @return the {@link PermissionsPolicySpec} to configure - * @deprecated For removal in 7.0. Use {@link #permissionsPolicy(Customizer)} - * instead. */ - @Deprecated(since = "6.1", forRemoval = true) public PermissionsPolicySpec permissionsPolicy() { return new PermissionsPolicySpec(); } @@ -2861,10 +2655,7 @@ public class ServerHttpSecurity { * Configures {@code Referrer-Policy} response header. * @param referrerPolicy the policy to use * @return the {@link ReferrerPolicySpec} to configure - * @deprecated For removal in 7.0. Use {@link #referrerPolicy(Customizer)} - * instead. */ - @Deprecated(since = "6.1", forRemoval = true) public ReferrerPolicySpec referrerPolicy(ReferrerPolicy referrerPolicy) { return new ReferrerPolicySpec(referrerPolicy); } @@ -2872,10 +2663,7 @@ public class ServerHttpSecurity { /** * Configures {@code Referrer-Policy} response header. * @return the {@link ReferrerPolicySpec} to configure - * @deprecated For removal in 7.0. Use {@link #referrerPolicy(Customizer)} - * instead. */ - @Deprecated(since = "6.1", forRemoval = true) public ReferrerPolicySpec referrerPolicy() { return new ReferrerPolicySpec(); } @@ -2897,11 +2685,8 @@ public class ServerHttpSecurity { * Cross-Origin-Opener-Policy header. * @return the {@link CrossOriginOpenerPolicySpec} to configure * @since 5.7 - * @deprecated For removal in 7.0. Use - * {@link #crossOriginOpenerPolicy(Customizer)} instead. * @see CrossOriginOpenerPolicyServerHttpHeadersWriter */ - @Deprecated(since = "6.1", forRemoval = true) public CrossOriginOpenerPolicySpec crossOriginOpenerPolicy() { return new CrossOriginOpenerPolicySpec(); } @@ -2926,11 +2711,8 @@ public class ServerHttpSecurity { * Cross-Origin-Embedder-Policy header. * @return the {@link CrossOriginEmbedderPolicySpec} to configure * @since 5.7 - * @deprecated For removal in 7.0. Use - * {@link #crossOriginEmbedderPolicy(Customizer)} instead. * @see CrossOriginEmbedderPolicyServerHttpHeadersWriter */ - @Deprecated(since = "6.1", forRemoval = true) public CrossOriginEmbedderPolicySpec crossOriginEmbedderPolicy() { return new CrossOriginEmbedderPolicySpec(); } @@ -2955,11 +2737,8 @@ public class ServerHttpSecurity { * Cross-Origin-Resource-Policy header. * @return the {@link CrossOriginResourcePolicySpec} to configure * @since 5.7 - * @deprecated For removal in 7.0. Use - * {@link #crossOriginResourcePolicy(Customizer)} instead. * @see CrossOriginResourcePolicyServerHttpHeadersWriter */ - @Deprecated(since = "6.1", forRemoval = true) public CrossOriginResourcePolicySpec crossOriginResourcePolicy() { return new CrossOriginResourcePolicySpec(); } @@ -3045,10 +2824,7 @@ public class ServerHttpSecurity { * Allows method chaining to continue configuring the * {@link ServerHttpSecurity} * @return the {@link HeaderSpec} to continue configuring - * @deprecated For removal in 7.0. Use {@link #frameOptions(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) private HeaderSpec and() { return HeaderSpec.this; } @@ -3116,13 +2892,7 @@ public class ServerHttpSecurity { * Allows method chaining to continue configuring the * {@link ServerHttpSecurity} * @return the {@link HeaderSpec} to continue configuring - * @deprecated For removal in 7.0. Use {@link #hsts(Customizer)} or - * {@code hsts(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public HeaderSpec and() { return HeaderSpec.this; } @@ -3211,10 +2981,7 @@ public class ServerHttpSecurity { * Allows method chaining to continue configuring the * {@link ServerHttpSecurity}. * @return the {@link HeaderSpec} to continue configuring - * @deprecated For removal in 7.0. Use - * {@link #contentSecurityPolicy(Customizer)} instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeaderSpec and() { return HeaderSpec.this; } @@ -3241,10 +3008,7 @@ public class ServerHttpSecurity { * Allows method chaining to continue configuring the * {@link ServerHttpSecurity}. * @return the {@link HeaderSpec} to continue configuring - * @deprecated For removal in 7.0. Use {@link #featurePolicy(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeaderSpec and() { return HeaderSpec.this; } @@ -3276,10 +3040,7 @@ public class ServerHttpSecurity { * Allows method chaining to continue configuring the * {@link ServerHttpSecurity}. * @return the {@link HeaderSpec} to continue configuring - * @deprecated For removal in 7.0. Use {@link #permissionsPolicy(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeaderSpec and() { return HeaderSpec.this; } @@ -3316,10 +3077,7 @@ public class ServerHttpSecurity { * Allows method chaining to continue configuring the * {@link ServerHttpSecurity}. * @return the {@link HeaderSpec} to continue configuring - * @deprecated For removal in 7.0. Use {@link #referrerPolicy(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeaderSpec and() { return HeaderSpec.this; } @@ -3350,10 +3108,7 @@ public class ServerHttpSecurity { * Allows method chaining to continue configuring the * {@link ServerHttpSecurity}. * @return the {@link HeaderSpec} to continue configuring - * @deprecated For removal in 7.0. Use - * {@link #crossOriginOpenerPolicy(Customizer)} instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeaderSpec and() { return HeaderSpec.this; } @@ -3384,10 +3139,7 @@ public class ServerHttpSecurity { * Allows method chaining to continue configuring the * {@link ServerHttpSecurity}. * @return the {@link HeaderSpec} to continue configuring - * @deprecated For removal in 7.0. Use - * {@link #crossOriginEmbedderPolicy(Customizer)} instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeaderSpec and() { return HeaderSpec.this; } @@ -3418,10 +3170,7 @@ public class ServerHttpSecurity { * Allows method chaining to continue configuring the * {@link ServerHttpSecurity}. * @return the {@link HeaderSpec} to continue configuring - * @deprecated For removal in 7.0. Use - * {@link #crossOriginResourcePolicy(Customizer)} instead */ - @Deprecated(since = "6.1", forRemoval = true) public HeaderSpec and() { return HeaderSpec.this; } @@ -3497,13 +3246,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #logout(Customizer)} or - * {@code logout(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -3614,13 +3357,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #cors(Customizer)} or - * {@code cors(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -3676,14 +3413,6 @@ public class ServerHttpSecurity { return this; } - /** - * @deprecated For removal in 7.0. Use {@link #x509(Customizer)} or - * {@code x509(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. - */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -3733,8 +3462,6 @@ public class ServerHttpSecurity { private ServerWebExchangeMatcher authenticationMatcher; - private ReactiveOidcSessionRegistry oidcSessionRegistry; - private ServerAuthenticationSuccessHandler authenticationSuccessHandler; private ServerAuthenticationFailureHandler authenticationFailureHandler; @@ -3766,20 +3493,6 @@ public class ServerHttpSecurity { return this; } - /** - * Configures the {@link ReactiveOidcSessionRegistry} to use when logins use OIDC. - * Default is to look the value up as a Bean, or else use an - * {@link InMemoryReactiveOidcSessionRegistry}. - * @param oidcSessionRegistry the registry to use - * @return the {@link OidcLogoutSpec} to customize - * @since 6.2 - */ - public OAuth2LoginSpec oidcSessionRegistry(ReactiveOidcSessionRegistry oidcSessionRegistry) { - Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null"); - this.oidcSessionRegistry = oidcSessionRegistry; - return this; - } - /** * The {@link ServerAuthenticationSuccessHandler} used after authentication * success. Defaults to {@link RedirectServerAuthenticationSuccessHandler} @@ -3952,13 +3665,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #oauth2Login(Customizer)} or - * {@code oauth2Login(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -3973,9 +3680,8 @@ public class ServerHttpSecurity { oauthRedirectFilter.setRequestCache(http.requestCache.requestCache); ReactiveAuthenticationManager manager = getAuthenticationManager(); - ReactiveOidcSessionRegistry sessionRegistry = getOidcSessionRegistry(); - AuthenticationWebFilter authenticationFilter = new OidcSessionRegistryAuthenticationWebFilter(manager, - authorizedClientRepository, sessionRegistry); + AuthenticationWebFilter authenticationFilter = new OAuth2LoginAuthenticationWebFilter(manager, + authorizedClientRepository); authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher()); authenticationFilter .setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository)); @@ -3984,8 +3690,6 @@ public class ServerHttpSecurity { authenticationFilter.setSecurityContextRepository(this.securityContextRepository); setDefaultEntryPoints(http); - http.addFilterAfter(new OidcSessionRegistryWebFilter(sessionRegistry), - SecurityWebFiltersOrder.HTTP_HEADERS_WRITER); http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC); http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION); } @@ -4030,16 +3734,6 @@ public class ServerHttpSecurity { http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint)); } - private ReactiveOidcSessionRegistry getOidcSessionRegistry() { - if (this.oidcSessionRegistry == null) { - this.oidcSessionRegistry = getBeanOrNull(ReactiveOidcSessionRegistry.class); - } - if (this.oidcSessionRegistry == null) { - this.oidcSessionRegistry = new InMemoryReactiveOidcSessionRegistry(); - } - return this.oidcSessionRegistry; - } - private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) { if (this.authenticationSuccessHandler == null) { RedirectServerAuthenticationSuccessHandler handler = new RedirectServerAuthenticationSuccessHandler(); @@ -4156,157 +3850,6 @@ public class ServerHttpSecurity { return new InMemoryReactiveOAuth2AuthorizedClientService(getClientRegistrationRepository()); } - private static final class OidcSessionRegistryWebFilter implements WebFilter { - - private final ReactiveOidcSessionRegistry oidcSessionRegistry; - - OidcSessionRegistryWebFilter(ReactiveOidcSessionRegistry oidcSessionRegistry) { - this.oidcSessionRegistry = oidcSessionRegistry; - } - - @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - return chain.filter(new OidcSessionRegistryServerWebExchange(exchange)); - } - - private final class OidcSessionRegistryServerWebExchange extends ServerWebExchangeDecorator { - - private final Mono sessionMono; - - protected OidcSessionRegistryServerWebExchange(ServerWebExchange delegate) { - super(delegate); - this.sessionMono = delegate.getSession().map(OidcSessionRegistryWebSession::new); - } - - @Override - public Mono getSession() { - return this.sessionMono; - } - - private final class OidcSessionRegistryWebSession implements WebSession { - - private final WebSession session; - - OidcSessionRegistryWebSession(WebSession session) { - this.session = session; - } - - @Override - public String getId() { - return this.session.getId(); - } - - @Override - public Map getAttributes() { - return this.session.getAttributes(); - } - - @Override - public void start() { - this.session.start(); - } - - @Override - public boolean isStarted() { - return this.session.isStarted(); - } - - @Override - public Mono changeSessionId() { - String currentId = this.session.getId(); - return this.session.changeSessionId() - .then(Mono.defer(() -> OidcSessionRegistryWebFilter.this.oidcSessionRegistry - .removeSessionInformation(currentId) - .flatMap((information) -> { - information = information.withSessionId(this.session.getId()); - return OidcSessionRegistryWebFilter.this.oidcSessionRegistry - .saveSessionInformation(information); - }))); - } - - @Override - public Mono invalidate() { - String currentId = this.session.getId(); - return this.session.invalidate() - .then(Mono.defer(() -> OidcSessionRegistryWebFilter.this.oidcSessionRegistry - .removeSessionInformation(currentId) - .then(Mono.empty()))); - } - - @Override - public Mono save() { - return this.session.save(); - } - - @Override - public boolean isExpired() { - return this.session.isExpired(); - } - - @Override - public Instant getCreationTime() { - return this.session.getCreationTime(); - } - - @Override - public Instant getLastAccessTime() { - return this.session.getLastAccessTime(); - } - - @Override - public void setMaxIdleTime(Duration maxIdleTime) { - this.session.setMaxIdleTime(maxIdleTime); - } - - @Override - public Duration getMaxIdleTime() { - return this.session.getMaxIdleTime(); - } - - } - - } - - } - - private static final class OidcSessionRegistryAuthenticationWebFilter - extends OAuth2LoginAuthenticationWebFilter { - - private final Log logger = LogFactory.getLog(getClass()); - - private final ReactiveOidcSessionRegistry oidcSessionRegistry; - - OidcSessionRegistryAuthenticationWebFilter(ReactiveAuthenticationManager authenticationManager, - ServerOAuth2AuthorizedClientRepository authorizedClientRepository, - ReactiveOidcSessionRegistry oidcSessionRegistry) { - super(authenticationManager, authorizedClientRepository); - this.oidcSessionRegistry = oidcSessionRegistry; - } - - @Override - protected Mono onAuthenticationSuccess(Authentication authentication, - WebFilterExchange webFilterExchange) { - if (!(authentication.getPrincipal() instanceof OidcUser user)) { - return super.onAuthenticationSuccess(authentication, webFilterExchange); - } - return webFilterExchange.getExchange().getSession().doOnNext((session) -> { - if (this.logger.isTraceEnabled()) { - this.logger.trace(String.format("Linking a provider [%s] session to this client's session", - user.getIssuer())); - } - }).flatMap((session) -> { - Mono csrfToken = webFilterExchange.getExchange().getAttribute(CsrfToken.class.getName()); - return (csrfToken != null) - ? csrfToken.map((token) -> new OidcSessionInformation(session.getId(), - Map.of(token.getHeaderName(), token.getToken()), user)) - : Mono.just(new OidcSessionInformation(session.getId(), Map.of(), user)); - }) - .flatMap(this.oidcSessionRegistry::saveSessionInformation) - .then(super.onAuthenticationSuccess(authentication, webFilterExchange)); - } - - } - } public final class OAuth2ClientSpec { @@ -4321,8 +3864,6 @@ public class ServerHttpSecurity { private ServerAuthorizationRequestRepository authorizationRequestRepository; - private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver; - private ServerRedirectStrategy authorizationRedirectStrategy; private OAuth2ClientSpec() { @@ -4417,26 +3958,6 @@ public class ServerHttpSecurity { return this.authorizationRequestRepository; } - /** - * Sets the resolver used for resolving {@link OAuth2AuthorizationRequest}'s. - * @param authorizationRequestResolver the resolver used for resolving - * {@link OAuth2AuthorizationRequest}'s - * @return the {@link OAuth2ClientSpec} to customize - * @since 6.1 - */ - public OAuth2ClientSpec authorizationRequestResolver( - ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver) { - this.authorizationRequestResolver = authorizationRequestResolver; - return this; - } - - private OAuth2AuthorizationRequestRedirectWebFilter getRedirectWebFilter() { - if (this.authorizationRequestResolver != null) { - return new OAuth2AuthorizationRequestRedirectWebFilter(this.authorizationRequestResolver); - } - return new OAuth2AuthorizationRequestRedirectWebFilter(getClientRegistrationRepository()); - } - /** * Sets the redirect strategy for Authorization Endpoint redirect URI. * @param authorizationRedirectStrategy the redirect strategy @@ -4457,18 +3978,13 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #oauth2Client(Customizer)} or - * {@code oauth2Client(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } protected void configure(ServerHttpSecurity http) { + ReactiveClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository(); ServerOAuth2AuthorizedClientRepository authorizedClientRepository = getAuthorizedClientRepository(); ServerAuthenticationConverter authenticationConverter = getAuthenticationConverter(); ReactiveAuthenticationManager authenticationManager = getAuthenticationManager(); @@ -4479,7 +3995,8 @@ public class ServerHttpSecurity { codeGrantWebFilter.setRequestCache(http.requestCache.requestCache); } - OAuth2AuthorizationRequestRedirectWebFilter oauthRedirectFilter = getRedirectWebFilter(); + OAuth2AuthorizationRequestRedirectWebFilter oauthRedirectFilter = new OAuth2AuthorizationRequestRedirectWebFilter( + clientRegistrationRepository); oauthRedirectFilter.setAuthorizationRequestRepository(getAuthorizationRequestRepository()); oauthRedirectFilter.setAuthorizationRedirectStrategy(getAuthorizationRedirectStrategy()); if (http.requestCache != null) { @@ -4611,12 +4128,7 @@ public class ServerHttpSecurity { /** * Enables JWT Resource Server support. * @return the {@link JwtSpec} for additional configuration - * @deprecated For removal in 7.0. Use {@link #jwt(Customizer)} or - * {@code jwt(Customizer.withDefaults())} to stick with defaults. See the documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public JwtSpec jwt() { if (this.jwt == null) { this.jwt = new JwtSpec(); @@ -4641,13 +4153,7 @@ public class ServerHttpSecurity { /** * Enables Opaque Token Resource Server support. * @return the {@link OpaqueTokenSpec} for additional configuration - * @deprecated For removal in 7.0. Use {@link #opaqueToken(Customizer)} or - * {@code opaqueToken(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public OpaqueTokenSpec opaqueToken() { if (this.opaqueToken == null) { this.opaqueToken = new OpaqueTokenSpec(); @@ -4739,11 +4245,6 @@ public class ServerHttpSecurity { return new ServerAuthenticationEntryPointFailureHandler(this.entryPoint); } - /** - * @deprecated For removal in 7.0. Use {@link #oauth2ResourceServer(Customizer)} - * instead - */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } @@ -4817,14 +4318,6 @@ public class ServerHttpSecurity { return this; } - /** - * @deprecated For removal in 7.0. Use {@link #jwt(Customizer)} or - * {@code jwt(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. - */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2ResourceServerSpec and() { return OAuth2ResourceServerSpec.this; } @@ -4935,10 +4428,7 @@ public class ServerHttpSecurity { * Allows method chaining to continue configuring the * {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #opaqueToken(Customizer)} - * instead */ - @Deprecated(since = "6.1", forRemoval = true) public OAuth2ResourceServerSpec and() { return OAuth2ResourceServerSpec.this; } @@ -4979,129 +4469,6 @@ public class ServerHttpSecurity { } - /** - * Configures OIDC 1.0 Logout support - * - * @author Josh Cummings - * @since 6.2 - */ - public final class OidcLogoutSpec { - - private ReactiveClientRegistrationRepository clientRegistrationRepository; - - private ReactiveOidcSessionRegistry sessionRegistry; - - private BackChannelLogoutConfigurer backChannel; - - /** - * Configures the {@link ReactiveClientRegistrationRepository}. Default is to look - * the value up as a Bean. - * @param clientRegistrationRepository the repository to use - * @return the {@link OidcLogoutSpec} to customize - */ - public OidcLogoutSpec clientRegistrationRepository( - ReactiveClientRegistrationRepository clientRegistrationRepository) { - Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); - this.clientRegistrationRepository = clientRegistrationRepository; - return this; - } - - /** - * Configures the {@link ReactiveOidcSessionRegistry}. Default is to use the value - * from {@link OAuth2LoginSpec#oidcSessionRegistry}, then look the value up as a - * Bean, or else use an {@link InMemoryReactiveOidcSessionRegistry}. - * @param sessionRegistry the registry to use - * @return the {@link OidcLogoutSpec} to customize - */ - public OidcLogoutSpec oidcSessionRegistry(ReactiveOidcSessionRegistry sessionRegistry) { - Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); - this.sessionRegistry = sessionRegistry; - return this; - } - - /** - * Configure OIDC Back-Channel Logout using the provided {@link Consumer} - * @return the {@link OidcLogoutSpec} for further configuration - */ - public OidcLogoutSpec backChannel(Customizer backChannelLogoutConfigurer) { - if (this.backChannel == null) { - this.backChannel = new OidcLogoutSpec.BackChannelLogoutConfigurer(); - } - backChannelLogoutConfigurer.customize(this.backChannel); - return this; - } - - @Deprecated(forRemoval = true, since = "6.2") - public ServerHttpSecurity and() { - return ServerHttpSecurity.this; - } - - void configure(ServerHttpSecurity http) { - if (this.backChannel != null) { - this.backChannel.configure(http); - } - } - - private ReactiveClientRegistrationRepository getClientRegistrationRepository() { - if (this.clientRegistrationRepository == null) { - this.clientRegistrationRepository = getBeanOrNull(ReactiveClientRegistrationRepository.class); - } - return this.clientRegistrationRepository; - } - - private ReactiveOidcSessionRegistry getSessionRegistry() { - if (this.sessionRegistry == null && ServerHttpSecurity.this.oauth2Login == null) { - return new InMemoryReactiveOidcSessionRegistry(); - } - if (this.sessionRegistry == null) { - return ServerHttpSecurity.this.oauth2Login.oidcSessionRegistry; - } - return this.sessionRegistry; - } - - /** - * A configurer for configuring OIDC Back-Channel Logout - */ - public final class BackChannelLogoutConfigurer { - - private ServerAuthenticationConverter authenticationConverter; - - private final ReactiveAuthenticationManager authenticationManager = new OidcBackChannelLogoutReactiveAuthenticationManager(); - - private ServerLogoutHandler logoutHandler; - - private ServerAuthenticationConverter authenticationConverter() { - if (this.authenticationConverter == null) { - this.authenticationConverter = new OidcLogoutServerAuthenticationConverter( - OidcLogoutSpec.this.getClientRegistrationRepository()); - } - return this.authenticationConverter; - } - - private ReactiveAuthenticationManager authenticationManager() { - return this.authenticationManager; - } - - private ServerLogoutHandler logoutHandler() { - if (this.logoutHandler == null) { - OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(); - logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry()); - this.logoutHandler = logoutHandler; - } - return this.logoutHandler; - } - - void configure(ServerHttpSecurity http) { - OidcBackChannelLogoutWebFilter filter = new OidcBackChannelLogoutWebFilter(authenticationConverter(), - authenticationManager()); - filter.setLogoutHandler(logoutHandler()); - http.addFilterBefore(filter, SecurityWebFiltersOrder.CSRF); - } - - } - - } - /** * Configures anonymous authentication * @@ -5187,13 +4554,7 @@ public class ServerHttpSecurity { /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring - * @deprecated For removal in 7.0. Use {@link #anonymous(Customizer)} or - * {@code anonymous(Customizer.withDefaults())} to stick with defaults. See the - * documentation - * for more details. */ - @Deprecated(since = "6.1", forRemoval = true) public ServerHttpSecurity and() { return ServerHttpSecurity.this; } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/FormLoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/FormLoginDsl.kt index 3a03ddf170..4f15900b3e 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/FormLoginDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/FormLoginDsl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 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. @@ -51,17 +51,6 @@ class FormLoginDsl { private var defaultSuccessUrlOption: Pair? = null - private var disabled = false - - /** - * Disable FormLogin. - * - * @since 6.1 - */ - fun disable() { - disabled = true - } - /** * Grants access to the urls for [failureUrl] as well as for the [HttpSecurityBuilder], the * [loginPage] and [loginProcessingUrl] for every user. @@ -95,9 +84,6 @@ class FormLoginDsl { authenticationSuccessHandler?.also { login.successHandler(authenticationSuccessHandler) } authenticationFailureHandler?.also { login.failureHandler(authenticationFailureHandler) } authenticationDetailsSource?.also { login.authenticationDetailsSource(authenticationDetailsSource) } - if (disabled) { - login.disable() - } } } } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt index 60342d2af8..8199217c8a 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.security.config.annotation.web -import jakarta.servlet.Filter -import jakarta.servlet.http.HttpServletRequest -import org.checkerframework.checker.units.qual.C import org.springframework.context.ApplicationContext import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.annotation.SecurityConfigurerAdapter @@ -27,6 +24,9 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository import org.springframework.security.web.DefaultSecurityFilterChain import org.springframework.security.web.util.matcher.RequestMatcher +import org.springframework.util.ClassUtils +import jakarta.servlet.Filter +import jakarta.servlet.http.HttpServletRequest /** * Configures [HttpSecurity] using a [HttpSecurity Kotlin DSL][HttpSecurityDsl]. @@ -107,36 +107,6 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu return this.http.apply(configurer).apply(configuration) } - /** - * Applies a [SecurityConfigurerAdapter] to this [HttpSecurity] - * - * Example: - * - * ``` - * @Configuration - * @EnableWebSecurity - * class SecurityConfig { - * - * @Bean - * fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - * http { - * with(CustomSecurityConfigurer()) { - * customProperty = "..." - * } - * } - * return http.build() - * } - * } - * ``` - * - * @param configurer - * the [HttpSecurity] for further customizations - * @since 6.2 - */ - fun > with(configurer: C, configuration: C.() -> Unit = { }): HttpSecurity? { - return this.http.with(configurer, configuration) - } - /** * Allows configuring the [HttpSecurity] to only be invoked when matching the * provided pattern. @@ -707,43 +677,6 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu this.http.saml2Login(saml2LoginCustomizer) } - /** - * Configures a SAML 2.0 relying party metadata endpoint. - * - * A [RelyingPartyRegistrationRepository] is required and must be registered with - * the [ApplicationContext] or configured via - * [Saml2Dsl.relyingPartyRegistrationRepository] - * - * Example: - * - * The following example shows the minimal configuration required, using a - * hypothetical asserting party. - * - * ``` - * @Configuration - * @EnableWebSecurity - * class SecurityConfig { - * - * @Bean - * fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - * http { - * saml2Login { } - * saml2Metadata { } - * } - * return http.build() - * } - * } - * ``` - * @param saml2MetadataConfiguration custom configuration to configure the - * SAML2 relying party metadata endpoint - * @see [Saml2MetadataDsl] - * @since 6.1 - */ - fun saml2Metadata(saml2MetadataConfiguration: Saml2MetadataDsl.() -> Unit) { - val saml2MetadataCustomizer = Saml2MetadataDsl().apply(saml2MetadataConfiguration).get() - this.http.saml2Metadata(saml2MetadataCustomizer) - } - /** * Allows configuring how an anonymous user is represented. * @@ -868,38 +801,6 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu this.http.oauth2ResourceServer(oauth2ResourceServerCustomizer) } - /** - * Configures OIDC 1.0 logout support. - * - * Example: - * - * ``` - * @Configuration - * @EnableWebSecurity - * class SecurityConfig { - * - * @Bean - * fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - * http { - * oauth2Login { } - * oidcLogout { - * backChannel { } - * } - * } - * return http.build() - * } - * } - * ``` - * - * @param oidcLogoutConfiguration custom configuration to configure the - * OIDC 1.0 logout support - * @see [OidcLogoutDsl] - */ - fun oidcLogout(oidcLogoutConfiguration: OidcLogoutDsl.() -> Unit) { - val oidcLogoutCustomizer = OidcLogoutDsl().apply(oidcLogoutConfiguration).get() - this.http.oidcLogout(oidcLogoutCustomizer) - } - /** * Configures Remember Me authentication. * diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt deleted file mode 100644 index f9fdd7dc4d..0000000000 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web - -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer -import org.springframework.security.config.annotation.web.oauth2.login.OidcBackChannelLogoutDsl -import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository - -/** - * A Kotlin DSL to configure [HttpSecurity] OAuth 1.0 Logout using idiomatic Kotlin code. - * - * @author Josh Cummings - * @since 6.2 - */ -@SecurityMarker -class OidcLogoutDsl { - var clientRegistrationRepository: ClientRegistrationRepository? = null - var oidcSessionRegistry: OidcSessionRegistry? = null - - private var backChannel: ((OidcLogoutConfigurer.BackChannelLogoutConfigurer) -> Unit)? = null - - /** - * Configures the OIDC 1.0 Back-Channel endpoint. - * - * Example: - * - * ``` - * @Configuration - * @EnableWebSecurity - * class SecurityConfig { - * - * @Bean - * fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - * http { - * oauth2Login { } - * oidcLogout { - * backChannel { } - * } - * } - * return http.build() - * } - * } - * ``` - * - * @param backChannelConfig custom configurations to configure the back-channel endpoint - * @see [OidcBackChannelLogoutDsl] - */ - fun backChannel(backChannelConfig: OidcBackChannelLogoutDsl.() -> Unit) { - this.backChannel = OidcBackChannelLogoutDsl().apply(backChannelConfig).get() - } - - internal fun get(): (OidcLogoutConfigurer) -> Unit { - return { oidcLogout -> - clientRegistrationRepository?.also { oidcLogout.clientRegistrationRepository(clientRegistrationRepository) } - oidcSessionRegistry?.also { oidcLogout.oidcSessionRegistry(oidcSessionRegistry) } - backChannel?.also { oidcLogout.backChannel(backChannel) } - } - } -} diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/Saml2MetadataDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/Saml2MetadataDsl.kt deleted file mode 100644 index f0d8b429e5..0000000000 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/Saml2MetadataDsl.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2002-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web - -import org.springframework.security.authentication.AuthenticationManagerResolver -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.oauth2.resourceserver.JwtDsl -import org.springframework.security.config.annotation.web.oauth2.resourceserver.OpaqueTokenDsl -import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer -import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver -import org.springframework.security.web.AuthenticationEntryPoint -import org.springframework.security.web.access.AccessDeniedHandler -import jakarta.servlet.http.HttpServletRequest -import org.springframework.security.config.annotation.web.configurers.saml2.Saml2MetadataConfigurer -import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver - -/** - * A Kotlin DSL to configure [HttpSecurity] SAML 2.0 relying party metadata support using - * idiomatic Kotlin code. - * - * @author Josh Cummings - * @since 6.1 - * @property metadataUrl the name of the relying party metadata endpoint; defaults to `/saml2/metadata` and `/saml2/metadata/{registrationId}` - * @property metadataResponseResolver the [Saml2MetadataResponseResolver] to use for resolving the - * metadata request into metadata - */ -@SecurityMarker -class Saml2MetadataDsl { - var metadataUrl: String? = null - var metadataResponseResolver: Saml2MetadataResponseResolver? = null - - internal fun get(): (Saml2MetadataConfigurer) -> Unit { - return { saml2Metadata -> - metadataResponseResolver?.also { saml2Metadata.metadataResponseResolver(metadataResponseResolver) } - metadataUrl?.also { saml2Metadata.metadataUrl(metadataUrl) } - } - } -} diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt deleted file mode 100644 index efac77a566..0000000000 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.oauth2.login - -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer - -/** - * A Kotlin DSL to configure the OIDC 1.0 Back-Channel configuration using - * idiomatic Kotlin code. - * - * @author Josh Cummings - * @since 6.2 - */ -@OAuth2LoginSecurityMarker -class OidcBackChannelLogoutDsl { - internal fun get(): (OidcLogoutConfigurer.BackChannelLogoutConfigurer) -> Unit { - return { backChannel -> } - } -} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt index 300a3d6a60..ce17734444 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -650,38 +650,6 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in this.http.oauth2ResourceServer(oauth2ResourceServerCustomizer) } - /** - * Configures logout support using an OpenID Connect 1.0 Provider. - * A [ReactiveClientRegistrationRepository] is required and must be registered as a Bean or - * configured via [ServerOidcLogoutDsl.clientRegistrationRepository]. - * - * Example: - * - * ``` - * @Configuration - * @EnableWebFluxSecurity - * class SecurityConfig { - * - * @Bean - * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - * return http { - * oauth2Login { } - * oidcLogout { - * backChannel { } - * } - * } - * } - * } - * ``` - * - * @param oidcLogoutConfiguration custom configuration to configure the OIDC 1.0 Logout - * @see [ServerOidcLogoutDsl] - */ - fun oidcLogout(oidcLogoutConfiguration: ServerOidcLogoutDsl.() -> Unit) { - val oidcLogoutCustomizer = ServerOidcLogoutDsl().apply(oidcLogoutConfiguration).get() - this.http.oidcLogout(oidcLogoutCustomizer) - } - /** * Apply all configurations to the provided [ServerHttpSecurity] */ diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt deleted file mode 100644 index 5a245e5092..0000000000 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server - -/** - * A Kotlin DSL to configure [ServerHttpSecurity] OIDC 1.0 Back-Channel Logout support using idiomatic Kotlin code. - * - * @author Josh Cummings - * @since 6.2 - */ -@ServerSecurityMarker -class ServerOidcBackChannelLogoutDsl { - internal fun get(): (ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer) -> Unit { - return { backChannel -> } - } -} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt deleted file mode 100644 index 503a5b0c84..0000000000 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server - -import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository - -/** - * A Kotlin DSL to configure [ServerHttpSecurity] OIDC 1.0 login using idiomatic Kotlin code. - * - * @author Josh Cummings - * @since 6.2 - */ -@ServerSecurityMarker -class ServerOidcLogoutDsl { - var clientRegistrationRepository: ReactiveClientRegistrationRepository? = null - var oidcSessionRegistry: ReactiveOidcSessionRegistry? = null - - private var backChannel: ((ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer) -> Unit)? = null - - /** - * Enables OIDC 1.0 Back-Channel Logout support. - * - * Example: - * - * ``` - * @Configuration - * @EnableWebFluxSecurity - * class SecurityConfig { - * - * @Bean - * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - * return http { - * oauth2Login { } - * oidcLogout { - * backChannel { } - * } - * } - * } - * } - * ``` - * - * @param backChannelConfig custom configurations to configure OIDC 1.0 Back-Channel Logout support - * @see [ServerOidcBackChannelLogoutDsl] - */ - fun backChannel(backChannelConfig: ServerOidcBackChannelLogoutDsl.() -> Unit) { - this.backChannel = ServerOidcBackChannelLogoutDsl().apply(backChannelConfig).get() - } - - internal fun get(): (ServerHttpSecurity.OidcLogoutSpec) -> Unit { - return { oidcLogout -> - clientRegistrationRepository?.also { oidcLogout.clientRegistrationRepository(clientRegistrationRepository) } - oidcSessionRegistry?.also { oidcLogout.oidcSessionRegistry(oidcSessionRegistry) } - backChannel?.also { oidcLogout.backChannel(backChannel) } - } - } -} diff --git a/config/src/main/resources/META-INF/spring.schemas b/config/src/main/resources/META-INF/spring.schemas index f25b5f682b..278137a694 100644 --- a/config/src/main/resources/META-INF/spring.schemas +++ b/config/src/main/resources/META-INF/spring.schemas @@ -1,6 +1,4 @@ -http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.2.xsd -http\://www.springframework.org/schema/security/spring-security-6.2.xsd=org/springframework/security/config/spring-security-6.2.xsd -http\://www.springframework.org/schema/security/spring-security-6.1.xsd=org/springframework/security/config/spring-security-6.1.xsd +http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.0.xsd http\://www.springframework.org/schema/security/spring-security-6.0.xsd=org/springframework/security/config/spring-security-6.0.xsd http\://www.springframework.org/schema/security/spring-security-5.8.xsd=org/springframework/security/config/spring-security-5.8.xsd http\://www.springframework.org/schema/security/spring-security-5.7.xsd=org/springframework/security/config/spring-security-5.7.xsd @@ -22,9 +20,7 @@ http\://www.springframework.org/schema/security/spring-security-2.0.xsd=org/spri http\://www.springframework.org/schema/security/spring-security-2.0.1.xsd=org/springframework/security/config/spring-security-2.0.1.xsd http\://www.springframework.org/schema/security/spring-security-2.0.2.xsd=org/springframework/security/config/spring-security-2.0.2.xsd http\://www.springframework.org/schema/security/spring-security-2.0.4.xsd=org/springframework/security/config/spring-security-2.0.4.xsd -https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.2.xsd -https\://www.springframework.org/schema/security/spring-security-6.2.xsd=org/springframework/security/config/spring-security-6.2.xsd -https\://www.springframework.org/schema/security/spring-security-6.1.xsd=org/springframework/security/config/spring-security-6.1.xsd +https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-6.0.xsd https\://www.springframework.org/schema/security/spring-security-6.0.xsd=org/springframework/security/config/spring-security-6.0.xsd https\://www.springframework.org/schema/security/spring-security-5.8.xsd=org/springframework/security/config/spring-security-5.8.xsd https\://www.springframework.org/schema/security/spring-security-5.7.xsd=org/springframework/security/config/spring-security-5.7.xsd diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.1.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-6.1.rnc deleted file mode 100644 index 7f89ced5af..0000000000 --- a/config/src/main/resources/org/springframework/security/config/spring-security-6.1.rnc +++ /dev/null @@ -1,1346 +0,0 @@ -namespace a = "https://relaxng.org/ns/compatibility/annotations/1.0" -datatypes xsd = "http://www.w3.org/2001/XMLSchema-datatypes" - -default namespace = "http://www.springframework.org/schema/security" - -start = http | ldap-server | authentication-provider | ldap-authentication-provider | any-user-service | ldap-server | ldap-authentication-provider - -hash = - ## Defines the hashing algorithm used on user passwords. Bcrypt is recommended. - attribute hash {"bcrypt"} -base64 = - ## Whether a string should be base64 encoded - attribute base64 {xsd:boolean} -request-matcher = - ## Defines the strategy use for matching incoming requests. Currently the options are 'mvc' (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for case-insensitive regular expressions. - attribute request-matcher {"mvc" | "ant" | "regex" | "ciRegex"} -port = - ## Specifies an IP port number. Used to configure an embedded LDAP server, for example. - attribute port { xsd:nonNegativeInteger } -url = - ## Specifies a URL. - attribute url { xsd:token } -id = - ## A bean identifier, used for referring to the bean elsewhere in the context. - attribute id {xsd:token} -name = - ## A bean identifier, used for referring to the bean elsewhere in the context. - attribute name {xsd:token} -ref = - ## Defines a reference to a Spring bean Id. - attribute ref {xsd:token} - -cache-ref = - ## Defines a reference to a cache for use with a UserDetailsService. - attribute cache-ref {xsd:token} - -user-service-ref = - ## A reference to a user-service (or UserDetailsService bean) Id - attribute user-service-ref {xsd:token} - -authentication-manager-ref = - ## A reference to an AuthenticationManager bean - attribute authentication-manager-ref {xsd:token} - -data-source-ref = - ## A reference to a DataSource bean - attribute data-source-ref {xsd:token} - - - -debug = - ## Enables Spring Security debugging infrastructure. This will provide human-readable (multi-line) debugging information to monitor requests coming into the security filters. This may include sensitive information, such as request parameters or headers, and should only be used in a development environment. - element debug {empty} - -password-encoder = - ## element which defines a password encoding strategy. Used by an authentication provider to convert submitted passwords to hashed versions, for example. - element password-encoder {password-encoder.attlist} -password-encoder.attlist &= - ref | (hash) - -role-prefix = - ## A non-empty string prefix that will be added to role strings loaded from persistent storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is non-empty. - attribute role-prefix {xsd:token} - -use-expressions = - ## Enables the use of expressions in the 'access' attributes in elements rather than the traditional list of configuration attributes. Defaults to 'true'. If enabled, each attribute should contain a single boolean expression. If the expression evaluates to 'true', access will be granted. - attribute use-expressions {xsd:boolean} - -ldap-server = - ## Defines an LDAP server location or starts an embedded server. The url indicates the location of a remote server. If no url is given, an embedded server will be started, listening on the supplied port number. The port is optional and defaults to 33389. A Spring LDAP ContextSource bean will be registered for the server with the id supplied. - element ldap-server {ldap-server.attlist} -ldap-server.attlist &= id? -ldap-server.attlist &= (url | port)? -ldap-server.attlist &= - ## Username (DN) of the "manager" user identity which will be used to authenticate to a (non-embedded) LDAP server. If omitted, anonymous access will be used. - attribute manager-dn {xsd:string}? -ldap-server.attlist &= - ## The password for the manager DN. This is required if the manager-dn is specified. - attribute manager-password {xsd:string}? -ldap-server.attlist &= - ## Explicitly specifies an ldif file resource to load into an embedded LDAP server. The default is classpath*:*.ldiff - attribute ldif { xsd:string }? -ldap-server.attlist &= - ## Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" - attribute root { xsd:string }? -ldap-server.attlist &= - ## Explicitly specifies which embedded ldap server should use. Values are 'apacheds' and 'unboundid'. By default, it will depends if the library is available in the classpath. - attribute mode { "apacheds" | "unboundid" }? - -ldap-server-ref-attribute = - ## The optional server to use. If omitted, and a default LDAP server is registered (using with no Id), that server will be used. - attribute server-ref {xsd:token} - - -group-search-filter-attribute = - ## Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN of the user. - attribute group-search-filter {xsd:token} -group-search-base-attribute = - ## Search base for group membership searches. Defaults to "" (searching from the root). - attribute group-search-base {xsd:token} -user-search-filter-attribute = - ## The LDAP filter used to search for users (optional). For example "(uid={0})". The substituted parameter is the user's login name. - attribute user-search-filter {xsd:token} -user-search-base-attribute = - ## Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. - attribute user-search-base {xsd:token} -group-role-attribute-attribute = - ## The LDAP attribute name which contains the role name which will be used within Spring Security. Defaults to "cn". - attribute group-role-attribute {xsd:token} -user-details-class-attribute = - ## Allows the objectClass of the user entry to be specified. If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object - attribute user-details-class {"person" | "inetOrgPerson"} -user-context-mapper-attribute = - ## Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry - attribute user-context-mapper-ref {xsd:token} - - -ldap-user-service = - ## This element configures a LdapUserDetailsService which is a combination of a FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. - element ldap-user-service {ldap-us.attlist} -ldap-us.attlist &= id? -ldap-us.attlist &= - ldap-server-ref-attribute? -ldap-us.attlist &= - user-search-filter-attribute? -ldap-us.attlist &= - user-search-base-attribute? -ldap-us.attlist &= - group-search-filter-attribute? -ldap-us.attlist &= - group-search-base-attribute? -ldap-us.attlist &= - group-role-attribute-attribute? -ldap-us.attlist &= - cache-ref? -ldap-us.attlist &= - role-prefix? -ldap-us.attlist &= - (user-details-class-attribute | user-context-mapper-attribute)? - -ldap-authentication-provider = - ## Sets up an ldap authentication provider - element ldap-authentication-provider {ldap-ap.attlist, password-compare-element?} -ldap-ap.attlist &= - ldap-server-ref-attribute? -ldap-ap.attlist &= - user-search-base-attribute? -ldap-ap.attlist &= - user-search-filter-attribute? -ldap-ap.attlist &= - group-search-base-attribute? -ldap-ap.attlist &= - group-search-filter-attribute? -ldap-ap.attlist &= - group-role-attribute-attribute? -ldap-ap.attlist &= - ## A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key "{0}" must be present and will be substituted with the username. - attribute user-dn-pattern {xsd:token}? -ldap-ap.attlist &= - role-prefix? -ldap-ap.attlist &= - (user-details-class-attribute | user-context-mapper-attribute)? - -password-compare-element = - ## Specifies that an LDAP provider should use an LDAP compare operation of the user's password to authenticate the user - element password-compare {password-compare.attlist, password-encoder?} - -password-compare.attlist &= - ## The attribute in the directory which contains the user password. Defaults to "userPassword". - attribute password-attribute {xsd:token}? -password-compare.attlist &= - hash? - -intercept-methods = - ## Can be used inside a bean definition to add a security interceptor to the bean and set up access configuration attributes for the bean's methods - element intercept-methods {intercept-methods.attlist, protect+} -intercept-methods.attlist &= - ## Optional AccessDecisionManager bean ID to be used by the created method security interceptor. - attribute access-decision-manager-ref {xsd:token}? -intercept-methods.attlist &= - ## Use the AuthorizationManager API instead of AccessDecisionManager (defaults to true) - attribute use-authorization-manager {xsd:boolean}? -intercept-methods.attlist &= - ## Use this AuthorizationManager instead of the default (supercedes use-authorization-manager) - attribute authorization-manager-ref {xsd:token}? - -protect = - ## Defines a protected method and the access control configuration attributes that apply to it. We strongly advise you NOT to mix "protect" declarations with any services provided "global-method-security". - element protect {protect.attlist, empty} -protect.attlist &= - ## A method name - attribute method {xsd:token} -protect.attlist &= - ## Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". - attribute access {xsd:token} - -method-security-metadata-source = - ## Creates a MethodSecurityMetadataSource instance - element method-security-metadata-source {msmds.attlist, protect+} -msmds.attlist &= id? - -msmds.attlist &= use-expressions? - -method-security = - ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with Spring Security annotations. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. Interceptors are invoked in the order specified in AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. Also, annotation-based interception can be overridden by expressions listed in elements. - element method-security {method-security.attlist, expression-handler?, protect-pointcut*} -method-security.attlist &= - ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "true". - attribute pre-post-enabled {xsd:boolean}? -method-security.attlist &= - ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "false". - attribute secured-enabled {xsd:boolean}? -method-security.attlist &= - ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "false". - attribute jsr250-enabled {xsd:boolean}? -method-security.attlist &= - ## If true, class-based proxying will be used instead of interface-based proxying. - attribute proxy-target-class {xsd:boolean}? -method-security.attlist &= - ## If set to aspectj, then use AspectJ to intercept method invocation - attribute mode {"aspectj"}? -method-security.attlist &= - ## Specifies the security context holder strategy to use, by default uses a ThreadLocal-based strategy - attribute security-context-holder-strategy-ref {xsd:string}? -method-security.attlist &= - ## Use this ObservationRegistry to collect metrics on various parts of the filter chain - attribute observation-registry-ref {xsd:token}? - -global-method-security = - ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with the ordered list of "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. If you use and enable all four sources of method security metadata (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 security annotations), the metadata sources will be queried in that order. In practical terms, this enables you to use XML to override method security metadata expressed in annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize etc.), @Secured and finally JSR-250. - element global-method-security {global-method-security.attlist, (pre-post-annotation-handling | expression-handler)?, protect-pointcut*, after-invocation-provider*} -global-method-security.attlist &= - ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "disabled". - attribute pre-post-annotations {"disabled" | "enabled" }? -global-method-security.attlist &= - ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "disabled". - attribute secured-annotations {"disabled" | "enabled" }? -global-method-security.attlist &= - ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "disabled". - attribute jsr250-annotations {"disabled" | "enabled" }? -global-method-security.attlist &= - ## Optional AccessDecisionManager bean ID to override the default used for method security. - attribute access-decision-manager-ref {xsd:token}? -global-method-security.attlist &= - ## Optional RunAsmanager implementation which will be used by the configured MethodSecurityInterceptor - attribute run-as-manager-ref {xsd:token}? -global-method-security.attlist &= - ## Allows the advice "order" to be set for the method security interceptor. - attribute order {xsd:token}? -global-method-security.attlist &= - ## If true, class based proxying will be used instead of interface based proxying. - attribute proxy-target-class {xsd:boolean}? -global-method-security.attlist &= - ## Can be used to specify that AspectJ should be used instead of the default Spring AOP. If set, secured classes must be woven with the AnnotationSecurityAspect from the spring-security-aspects module. - attribute mode {"aspectj"}? -global-method-security.attlist &= - ## An external MethodSecurityMetadataSource instance can be supplied which will take priority over other sources (such as the default annotations). - attribute metadata-source-ref {xsd:token}? -global-method-security.attlist &= - authentication-manager-ref? - - -after-invocation-provider = - ## Allows addition of extra AfterInvocationProvider beans which should be called by the MethodSecurityInterceptor created by global-method-security. - element after-invocation-provider {ref} - -pre-post-annotation-handling = - ## Allows the default expression-based mechanism for handling Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be replace entirely. Only applies if these annotations are enabled. - element pre-post-annotation-handling {invocation-attribute-factory, pre-invocation-advice, post-invocation-advice} - -invocation-attribute-factory = - ## Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and post invocation metadata from the annotated methods. - element invocation-attribute-factory {ref} - -pre-invocation-advice = - ## Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the PreInvocationAuthorizationAdviceVoter for the element. - element pre-invocation-advice {ref} - -post-invocation-advice = - ## Customizes the PostInvocationAdviceProvider with the ref as the PostInvocationAuthorizationAdvice for the element. - element post-invocation-advice {ref} - - -expression-handler = - ## Defines the SecurityExpressionHandler instance which will be used if expression-based access-control is enabled. A default implementation (with no ACL support) will be used if not supplied. - element expression-handler {ref} - -protect-pointcut = - ## Defines a protected pointcut and the access control configuration attributes that apply to it. Every bean registered in the Spring application context that provides a method that matches the pointcut will receive security authorization. - element protect-pointcut {protect-pointcut.attlist, empty} -protect-pointcut.attlist &= - ## An AspectJ expression, including the 'execution' keyword. For example, 'execution(int com.foo.TargetObject.countLength(String))' (without the quotes). - attribute expression {xsd:string} -protect-pointcut.attlist &= - ## Access configuration attributes list that applies to all methods matching the pointcut, e.g. "ROLE_A,ROLE_B" - attribute access {xsd:token} - -websocket-message-broker = - ## Allows securing a Message Broker. There are two modes. If no id is specified: ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that can be manually registered with the clientInboundChannel. - element websocket-message-broker { websocket-message-broker.attrlist, (intercept-message* & expression-handler?) } - -websocket-message-broker.attrlist &= - ## A bean identifier, used for referring to the bean elsewhere in the context. If specified, explicit configuration within clientInboundChannel is required. If not specified, ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. - attribute id {xsd:token}? -websocket-message-broker.attrlist &= - ## Disables the requirement for CSRF token to be present in the Stomp headers (default false). Changing the default is useful if it is necessary to allow other origins to make SockJS connections. - attribute same-origin-disabled {xsd:boolean}? -websocket-message-broker.attrlist &= - ## Use this AuthorizationManager instead of deriving one from elements - attribute authorization-manager-ref {xsd:string}? -websocket-message-broker.attrlist &= - ## Use AuthorizationManager API instead of SecurityMetadatasource (defaults to true) - attribute use-authorization-manager {xsd:boolean}? -websocket-message-broker.attrlist &= - ## Use this SecurityContextHolderStrategy (note only supported in conjunction with the AuthorizationManager API) - attribute security-context-holder-strategy-ref {xsd:string}? - -intercept-message = - ## Creates an authorization rule for a websocket message. - element intercept-message {intercept-message.attrlist} - -intercept-message.attrlist &= - ## The destination ant pattern which will be mapped to the access attribute. For example, /** matches any message with a destination, /admin/** matches any message that has a destination that starts with admin. - attribute pattern {xsd:token}? -intercept-message.attrlist &= - ## The access configuration attributes that apply for the configured message. For example, permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role 'ROLE_ADMIN'. - attribute access {xsd:token}? -intercept-message.attrlist &= - ## The type of message to match on. Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER). - attribute type {"CONNECT" | "CONNECT_ACK" | "HEARTBEAT" | "MESSAGE" | "SUBSCRIBE"| "UNSUBSCRIBE" | "DISCONNECT" | "DISCONNECT_ACK" | "OTHER"}? - -http-firewall = - ## Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created by the namespace. - element http-firewall {ref} - -http = - ## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "security" attribute to "none". - element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & oauth2-login? & oauth2-client? & oauth2-resource-server? & saml2-login? & saml2-logout? & x509? & jee? & http-basic? & logout? & password-management? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) } -http.attlist &= - ## The request URL pattern which will be mapped to the filter chain created by this element. If omitted, the filter chain will match all requests. - attribute pattern {xsd:token}? -http.attlist &= - ## When set to 'none', requests matching the pattern attribute will be ignored by Spring Security. No security filters will be applied and no SecurityContext will be available. If set, the element must be empty, with no children. - attribute security {"none"}? -http.attlist &= - ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. - attribute request-matcher-ref { xsd:token }? -http.attlist &= - ## A legacy attribute which automatically registers a login form, BASIC authentication and a logout URL and logout services. If unspecified, defaults to "false". We'd recommend you avoid using this and instead explicitly configure the services you require. - attribute auto-config {xsd:boolean}? -http.attlist &= - use-expressions? -http.attlist &= - ## A reference to a SecurityContextHolderStrategy bean. This can be used to customize how the SecurityContextHolder is stored during a request - attribute security-context-holder-strategy-ref {xsd:token}? -http.attlist &= - ## Controls the eagerness with which an HTTP session is created by Spring Security classes. If not set, defaults to "ifRequired". If "stateless" is used, this implies that the application guarantees that it will not create a session. This differs from the use of "never" which means that Spring Security will not create a session, but will make use of one if the application does. - attribute create-session {"ifRequired" | "always" | "never" | "stateless"}? -http.attlist &= - ## A reference to a SecurityContextRepository bean. This can be used to customize how the SecurityContext is stored between requests. - attribute security-context-repository-ref {xsd:token}? -http.attlist &= - ## Optional attribute that specifies that the SecurityContext should require explicit saving rather than being synchronized from the SecurityContextHolder. Defaults to "true". - attribute security-context-explicit-save {xsd:boolean}? -http.attlist &= - request-matcher? -http.attlist &= - ## Provides versions of HttpServletRequest security methods such as isUserInRole() and getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to "true". - attribute servlet-api-provision {xsd:boolean}? -http.attlist &= - ## If available, runs the request as the Subject acquired from the JaasAuthenticationToken. Defaults to "false". - attribute jaas-api-provision {xsd:boolean}? -http.attlist &= - ## Use AuthorizationManager API instead of SecurityMetadataSource (defaults to true) - attribute use-authorization-manager {xsd:boolean}? -http.attlist &= - ## Use this AuthorizationManager instead of deriving one from elements - attribute authorization-manager-ref {xsd:token}? -http.attlist &= - ## Optional attribute specifying the ID of the AccessDecisionManager implementation which should be used for authorizing HTTP requests. - attribute access-decision-manager-ref {xsd:token}? -http.attlist &= - ## Optional attribute specifying the realm name that will be used for all authentication features that require a realm name (eg BASIC and Digest authentication). If unspecified, defaults to "Spring Security Application". - attribute realm {xsd:token}? -http.attlist &= - ## Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. - attribute entry-point-ref {xsd:token}? -http.attlist &= - ## Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults to "false" - attribute once-per-request {xsd:boolean}? -http.attlist &= - ## Corresponds to the shouldFilterAllDispatcherTypes property of AuthorizationFilter. Do not work when use-authorization-manager=false. Defaults to "true". - attribute filter-all-dispatcher-types {xsd:boolean}? -http.attlist &= - ## Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" (rewriting is disabled). - attribute disable-url-rewriting {xsd:boolean}? -http.attlist &= - ## Exposes the list of filters defined by this configuration under this bean name in the application context. - name? -http.attlist &= - authentication-manager-ref? -http.attlist &= - ## Use this ObservationRegistry to collect metrics on various parts of the filter chain - attribute observation-registry-ref {xsd:token}? - -access-denied-handler = - ## Defines the access-denied strategy that should be used. An access denied page can be defined or a reference to an AccessDeniedHandler instance. - element access-denied-handler {access-denied-handler.attlist, empty} -access-denied-handler.attlist &= (ref | access-denied-handler-page) - -access-denied-handler-page = - ## The access denied page that an authenticated user will be redirected to if they request a page which they don't have the authority to access. - attribute error-page {xsd:token} - -intercept-url = - ## Specifies the access attributes and/or filter list for a particular set of URLs. - element intercept-url {intercept-url.attlist, empty} -intercept-url.attlist &= - (pattern | request-matcher-ref) -intercept-url.attlist &= - ## The access configuration attributes that apply for the configured path. - attribute access {xsd:token}? -intercept-url.attlist &= - ## The HTTP Method for which the access configuration attributes should apply. If not specified, the attributes will apply to any method. - attribute method {"GET" | "DELETE" | "HEAD" | "OPTIONS" | "POST" | "PUT" | "PATCH" | "TRACE"}? - -intercept-url.attlist &= - ## Used to specify that a URL must be accessed over http or https, or that there is no preference. The value should be "http", "https" or "any", respectively. - attribute requires-channel {xsd:token}? -intercept-url.attlist &= - ## The path to the servlet. This attribute is only applicable when 'request-matcher' is 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are 2 or more HttpServlet's registered in the ServletContext that have mappings starting with '/' and are different; 2) The pattern starts with the same value of a registered HttpServlet path, excluding the default (root) HttpServlet '/'. - attribute servlet-path {xsd:token}? - -logout = - ## Incorporates a logout processing filter. Most web applications require a logout filter, although you may not require one if you write a controller to provider similar logic. - element logout {logout.attlist, empty} -logout.attlist &= - ## Specifies the URL that will cause a logout. Spring Security will initialize a filter that responds to this particular URL. Defaults to /logout if unspecified. - attribute logout-url {xsd:token}? -logout.attlist &= - ## Specifies the URL to display once the user has logged out. If not specified, defaults to /?logout (i.e. /login?logout). - attribute logout-success-url {xsd:token}? -logout.attlist &= - ## Specifies whether a logout also causes HttpSession invalidation, which is generally desirable. If unspecified, defaults to true. - attribute invalidate-session {xsd:boolean}? -logout.attlist &= - ## A reference to a LogoutSuccessHandler implementation which will be used to determine the destination to which the user is taken after logging out. - attribute success-handler-ref {xsd:token}? -logout.attlist &= - ## A comma-separated list of the names of cookies which should be deleted when the user logs out - attribute delete-cookies {xsd:token}? - -request-cache = - ## Allow the RequestCache used for saving requests during the login process to be set - element request-cache {ref} - -form-login = - ## Sets up a form login configuration for authentication with a username and password - element form-login {form-login.attlist, empty} -form-login.attlist &= - ## The URL that the login form is posted to. If unspecified, it defaults to /login. - attribute login-processing-url {xsd:token}? -form-login.attlist &= - ## The name of the request parameter which contains the username. Defaults to 'username'. - attribute username-parameter {xsd:token}? -form-login.attlist &= - ## The name of the request parameter which contains the password. Defaults to 'password'. - attribute password-parameter {xsd:token}? -form-login.attlist &= - ## The URL that will be redirected to after successful authentication, if the user's previous action could not be resumed. This generally happens if the user visits a login page without having first requested a secured operation that triggers authentication. If unspecified, defaults to the root of the application. - attribute default-target-url {xsd:token}? -form-login.attlist &= - ## Whether the user should always be redirected to the default-target-url after login. - attribute always-use-default-target {xsd:boolean}? -form-login.attlist &= - ## The URL for the login page. If no login URL is specified, Spring Security will automatically create a login URL at GET /login and a corresponding filter to render that login URL when requested. - attribute login-page {xsd:token}? -form-login.attlist &= - ## The URL for the login failure page. If no login failure URL is specified, Spring Security will automatically create a failure login URL at /login?error and a corresponding filter to render that login failure URL when requested. - attribute authentication-failure-url {xsd:token}? -form-login.attlist &= - ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful authentication request. Should not be used in combination with default-target-url (or always-use-default-target-url) as the implementation should always deal with navigation to the subsequent destination - attribute authentication-success-handler-ref {xsd:token}? -form-login.attlist &= - ## Reference to an AuthenticationFailureHandler bean which should be used to handle a failed authentication request. Should not be used in combination with authentication-failure-url as the implementation should always deal with navigation to the subsequent destination - attribute authentication-failure-handler-ref {xsd:token}? -form-login.attlist &= - ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter - attribute authentication-details-source-ref {xsd:token}? -form-login.attlist &= - ## The URL for the ForwardAuthenticationFailureHandler - attribute authentication-failure-forward-url {xsd:token}? -form-login.attlist &= - ## The URL for the ForwardAuthenticationSuccessHandler - attribute authentication-success-forward-url {xsd:token}? - -oauth2-login = - ## Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. - element oauth2-login {oauth2-login.attlist} -oauth2-login.attlist &= - ## Reference to the ClientRegistrationRepository - attribute client-registration-repository-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the OAuth2AuthorizedClientRepository - attribute authorized-client-repository-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the OAuth2AuthorizedClientService - attribute authorized-client-service-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the AuthorizationRequestRepository - attribute authorization-request-repository-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the OAuth2AuthorizationRequestResolver - attribute authorization-request-resolver-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the authorization RedirectStrategy - attribute authorization-redirect-strategy-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the OAuth2AccessTokenResponseClient - attribute access-token-response-client-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the GrantedAuthoritiesMapper - attribute user-authorities-mapper-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the OAuth2UserService - attribute user-service-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the OpenID Connect OAuth2UserService - attribute oidc-user-service-ref {xsd:token}? -oauth2-login.attlist &= - ## The URI where the filter processes authentication requests - attribute login-processing-url {xsd:token}? -oauth2-login.attlist &= - ## The URI to send users to login - attribute login-page {xsd:token}? -oauth2-login.attlist &= - ## Reference to the AuthenticationSuccessHandler - attribute authentication-success-handler-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the AuthenticationFailureHandler - attribute authentication-failure-handler-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider - attribute jwt-decoder-factory-ref {xsd:token}? - -oauth2-client = - ## Configures OAuth 2.0 Client support. - element oauth2-client {oauth2-client.attlist, (authorization-code-grant?) } -oauth2-client.attlist &= - ## Reference to the ClientRegistrationRepository - attribute client-registration-repository-ref {xsd:token}? -oauth2-client.attlist &= - ## Reference to the OAuth2AuthorizedClientRepository - attribute authorized-client-repository-ref {xsd:token}? -oauth2-client.attlist &= - ## Reference to the OAuth2AuthorizedClientService - attribute authorized-client-service-ref {xsd:token}? - -authorization-code-grant = - ## Configures OAuth 2.0 Authorization Code Grant. - element authorization-code-grant {authorization-code-grant.attlist, empty} -authorization-code-grant.attlist &= - ## Reference to the AuthorizationRequestRepository - attribute authorization-request-repository-ref {xsd:token}? -authorization-code-grant.attlist &= - ## Reference to the authorization RedirectStrategy - attribute authorization-redirect-strategy-ref {xsd:token}? -authorization-code-grant.attlist &= - ## Reference to the OAuth2AuthorizationRequestResolver - attribute authorization-request-resolver-ref {xsd:token}? -authorization-code-grant.attlist &= - ## Reference to the OAuth2AccessTokenResponseClient - attribute access-token-response-client-ref {xsd:token}? - -client-registrations = - ## Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. - element client-registrations {client-registration+, provider*} - -client-registration = - ## Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. - element client-registration {client-registration.attlist} -client-registration.attlist &= - ## The ID that uniquely identifies the client registration. - attribute registration-id {xsd:token} -client-registration.attlist &= - ## The client identifier. - attribute client-id {xsd:token} -client-registration.attlist &= - ## The client secret. - attribute client-secret {xsd:token}? -client-registration.attlist &= - ## The method used to authenticate the client with the provider. The supported values are client_secret_basic, client_secret_post and none (public clients). - attribute client-authentication-method {"client_secret_basic" | "basic" | "client_secret_post" | "post" | "none"}? -client-registration.attlist &= - ## The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The supported values are authorization_code, client_credentials and password. - attribute authorization-grant-type {"authorization_code" | "client_credentials" | "password"}? -client-registration.attlist &= - ## The client’s registered redirect URI that the Authorization Server redirects the end-user’s user-agent to after the end-user has authenticated and authorized access to the client. - attribute redirect-uri {xsd:token}? -client-registration.attlist &= - ## A comma-separated list of scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. - attribute scope {xsd:token}? -client-registration.attlist &= - ## A descriptive name used for the client. The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. - attribute client-name {xsd:token}? -client-registration.attlist &= - ## A reference to the associated provider. May reference a 'provider' element or use one of the common providers (google, github, facebook, okta). - attribute provider-id {xsd:token} - -provider = - ## The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. - element provider {provider.attlist} -provider.attlist &= - ## The ID that uniquely identifies the provider. - attribute provider-id {xsd:token} -provider.attlist &= - ## The Authorization Endpoint URI for the Authorization Server. - attribute authorization-uri {xsd:token}? -provider.attlist &= - ## The Token Endpoint URI for the Authorization Server. - attribute token-uri {xsd:token}? -provider.attlist &= - ## The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. - attribute user-info-uri {xsd:token}? -provider.attlist &= - ## The authentication method used when sending the access token to the UserInfo Endpoint. The supported values are header, form and query. - attribute user-info-authentication-method {"header" | "form" | "query"}? -provider.attlist &= - ## The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. - attribute user-info-user-name-attribute {xsd:token}? -provider.attlist &= - ## The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server, which contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID Token and optionally the UserInfo Response. - attribute jwk-set-uri {xsd:token}? -provider.attlist &= - ## The URI used to discover the configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. - attribute issuer-uri {xsd:token}? - -oauth2-resource-server = - ## Configures authentication support as an OAuth 2.0 Resource Server. - element oauth2-resource-server {oauth2-resource-server.attlist, (jwt? & opaque-token?)} -oauth2-resource-server.attlist &= - ## Reference to an AuthenticationManagerResolver - attribute authentication-manager-resolver-ref {xsd:token}? -oauth2-resource-server.attlist &= - ## Reference to a BearerTokenResolver - attribute bearer-token-resolver-ref {xsd:token}? -oauth2-resource-server.attlist &= - ## Reference to a AuthenticationEntryPoint - attribute entry-point-ref {xsd:token}? - -jwt = - ## Configures JWT authentication - element jwt {jwt.attlist} -jwt.attlist &= - ## The URI to use to collect the JWK Set for verifying JWTs - attribute jwk-set-uri {xsd:token}? -jwt.attlist &= - ## Reference to a JwtDecoder - attribute decoder-ref {xsd:token}? -jwt.attlist &= - ## Reference to a Converter - attribute jwt-authentication-converter-ref {xsd:token}? - -opaque-token = - ## Configuration Opaque Token authentication - element opaque-token {opaque-token.attlist} -opaque-token.attlist &= - ## The URI to use to introspect opaque token attributes - attribute introspection-uri {xsd:token}? -opaque-token.attlist &= - ## The Client ID to use to authenticate the introspection request - attribute client-id {xsd:token}? -opaque-token.attlist &= - ## The Client secret to use to authenticate the introspection request - attribute client-secret {xsd:token}? -opaque-token.attlist &= - ## Reference to an OpaqueTokenIntrospector - attribute introspector-ref {xsd:token}? -opaque-token.attlist &= - ## Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful introspection result into an Authentication. - attribute authentication-converter-ref {xsd:token}? - -saml2-login = - ## Configures authentication support for SAML 2.0 Login - element saml2-login {saml2-login.attlist} -saml2-login.attlist &= - ## Reference to the RelyingPartyRegistrationRepository - attribute relying-party-registration-repository-ref {xsd:token}? -saml2-login.attlist &= - ## Reference to the Saml2AuthenticationRequestRepository - attribute authentication-request-repository-ref {xsd:token}? -saml2-login.attlist &= - ## Reference to the Saml2AuthenticationRequestResolver - attribute authentication-request-resolver-ref {xsd:token}? -saml2-login.attlist &= - ## Reference to the AuthenticationConverter - attribute authentication-converter-ref {xsd:token}? -saml2-login.attlist &= - ## The URI where the filter processes authentication requests - attribute login-processing-url {xsd:token}? -saml2-login.attlist &= - ## The URI to send users to login - attribute login-page {xsd:token}? -saml2-login.attlist &= - ## Reference to the AuthenticationSuccessHandler - attribute authentication-success-handler-ref {xsd:token}? -saml2-login.attlist &= - ## Reference to the AuthenticationFailureHandler - attribute authentication-failure-handler-ref {xsd:token}? -saml2-login.attlist &= - ## Reference to the AuthenticationManager - attribute authentication-manager-ref {xsd:token}? - -saml2-logout = - ## Configures SAML 2.0 Single Logout support - element saml2-logout {saml2-logout.attlist} -saml2-logout.attlist &= - ## The URL by which the relying or asserting party can trigger logout - attribute logout-url {xsd:token}? -saml2-logout.attlist &= - ## The URL by which the asserting party can send a SAML 2.0 Logout Request - attribute logout-request-url {xsd:token}? -saml2-logout.attlist &= - ## The URL by which the asserting party can send a SAML 2.0 Logout Response - attribute logout-response-url {xsd:token}? -saml2-logout.attlist &= - ## Reference to the RelyingPartyRegistrationRepository - attribute relying-party-registration-repository-ref {xsd:token}? -saml2-logout.attlist &= - ## Reference to the Saml2LogoutRequestValidator - attribute logout-request-validator-ref {xsd:token}? -saml2-logout.attlist &= - ## Reference to the Saml2LogoutRequestResolver - attribute logout-request-resolver-ref {xsd:token}? -saml2-logout.attlist &= - ## Reference to the Saml2LogoutRequestRepository - attribute logout-request-repository-ref {xsd:token}? -saml2-logout.attlist &= - ## Reference to the Saml2LogoutResponseValidator - attribute logout-response-validator-ref {xsd:token}? -saml2-logout.attlist &= - ## Reference to the Saml2LogoutResponseResolver - attribute logout-response-resolver-ref {xsd:token}? - -relying-party-registrations = - ## Container element for relying party(ies) registered with a SAML 2.0 identity provider - element relying-party-registrations {relying-party-registration+, asserting-party*} - -relying-party-registration = - ## Represents a relying party registered with a SAML 2.0 identity provider - element relying-party-registration {relying-party-registration.attlist, signing-credential*, decryption-credential*} -relying-party-registration.attlist &= - ## The ID that uniquely identifies the relying party registration. - attribute registration-id {xsd:token} -relying-party-registration.attlist &= - ## The location of the Identity Provider's metadata. - attribute metadata-location {xsd:token}? -relying-party-registration.attlist &= - ## The relying party's EntityID - attribute entity-id {xsd:token}? -relying-party-registration.attlist &= - ## The Assertion Consumer Service Location - attribute assertion-consumer-service-location {xsd:token}? -relying-party-registration.attlist &= - ## The Assertion Consumer Service Binding - attribute assertion-consumer-service-binding {xsd:token}? -relying-party-registration.attlist &= - ## A reference to the associated asserting party. - attribute asserting-party-id {xsd:token}? -relying-party-registration.attlist &= - ## The relying party SingleLogoutService Location - attribute single-logout-service-location {xsd:token}? -relying-party-registration.attlist &= - ## The relying party SingleLogoutService Response Location - attribute single-logout-service-response-location {xsd:token}? -relying-party-registration.attlist &= - ## The relying party SingleLogoutService Binding - attribute single-logout-service-binding {xsd:token}? - -signing-credential = - ## The relying party's signing credential - element signing-credential {signing-credential.attlist} -signing-credential.attlist &= - ## The private key location - attribute private-key-location {xsd:token} -signing-credential.attlist &= - ## The certificate location - attribute certificate-location {xsd:token} - -decryption-credential = - ## The relying party's decryption credential - element decryption-credential {decryption-credential.attlist} -decryption-credential.attlist &= - ## The private key location - attribute private-key-location {xsd:token} -decryption-credential.attlist &= - ## The certificate location - attribute certificate-location {xsd:token} - -asserting-party = - ## The configuration metadata of the Asserting party - element asserting-party {asserting-party.attlist, verification-credential*, encryption-credential*} -asserting-party.attlist &= - ## A unique identifier of the asserting party. - attribute asserting-party-id {xsd:token} -asserting-party.attlist &= - ## The asserting party's EntityID. - attribute entity-id {xsd:token} -asserting-party.attlist &= - ## Indicates the asserting party's preference that relying parties should sign the AuthnRequest before sending - attribute want-authn-requests-signed {xsd:token}? -asserting-party.attlist &= - ## The SingleSignOnService Location. - attribute single-sign-on-service-location {xsd:token} -asserting-party.attlist &= - ## The SingleSignOnService Binding. - attribute single-sign-on-service-binding {xsd:token}? -asserting-party.attlist &= - ## A comma separated list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this asserting party, in preference order. - attribute signing-algorithms {xsd:token}? -asserting-party.attlist &= - ## The asserting party SingleLogoutService Location - attribute single-logout-service-location {xsd:token}? -asserting-party.attlist &= - ## The asserting party SingleLogoutService Response Location - attribute single-logout-service-response-location {xsd:token}? -asserting-party.attlist &= - ## The asserting party SingleLogoutService Binding - attribute single-logout-service-binding {xsd:token}? - -verification-credential = - ## The relying party's verification credential - element verification-credential {verification-credential.attlist} -verification-credential.attlist &= - ## The private key location - attribute private-key-location {xsd:token} -verification-credential.attlist &= - ## The certificate location - attribute certificate-location {xsd:token} - -encryption-credential = - ## The asserting party's encryption credential - element encryption-credential {encryption-credential.attlist} -encryption-credential.attlist &= - ## The private key location - attribute private-key-location {xsd:token} -encryption-credential.attlist &= - ## The certificate location - attribute certificate-location {xsd:token} - - -filter-chain-map = - ## Used to explicitly configure a FilterChainProxy instance with a FilterChainMap - element filter-chain-map {filter-chain-map.attlist, filter-chain+} -filter-chain-map.attlist &= - request-matcher? - -filter-chain = - ## Used within to define a specific URL pattern and the list of filters which apply to the URLs matching that pattern. When multiple filter-chain elements are assembled in a list in order to configure a FilterChainProxy, the most specific patterns must be placed at the top of the list, with most general ones at the bottom. - element filter-chain {filter-chain.attlist, empty} -filter-chain.attlist &= - (pattern | request-matcher-ref) -filter-chain.attlist &= - ## A comma separated list of bean names that implement Filter that should be processed for this FilterChain. If the value is none, then no Filters will be used for this FilterChain. - attribute filters {xsd:token} - -pattern = - ## The request URL pattern which will be mapped to the FilterChain. - attribute pattern {xsd:token} -request-matcher-ref = - ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. - attribute request-matcher-ref {xsd:token} - -filter-security-metadata-source = - ## Used to explicitly configure a FilterSecurityMetadataSource bean for use with a FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy explicitly, rather than using the element. The intercept-url elements used should only contain pattern, method and access attributes. Any others will result in a configuration error. - element filter-security-metadata-source {fsmds.attlist, intercept-url+} -fsmds.attlist &= - use-expressions? -fsmds.attlist &= - id? -fsmds.attlist &= - request-matcher? - -http-basic = - ## Adds support for basic authentication - element http-basic {http-basic.attlist, empty} - -http-basic.attlist &= - ## Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. - attribute entry-point-ref {xsd:token}? -http-basic.attlist &= - ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter - attribute authentication-details-source-ref {xsd:token}? - -password-management = - ## Adds support for the password management. - element password-management {password-management.attlist, empty} - -password-management.attlist &= - ## The change password page. Defaults to "/change-password". - attribute change-password-page {xsd:string}? - -session-management = - ## Session-management related functionality is implemented by the addition of a SessionManagementFilter to the filter stack. - element session-management {session-management.attlist, concurrency-control?} - -session-management.attlist &= - ## Specifies that SessionAuthenticationStrategy must be explicitly invoked. Default false (i.e. SessionManagementFilter will implicitly invoke SessionAuthenticationStrategy). - attribute authentication-strategy-explicit-invocation {xsd:boolean}? -session-management.attlist &= - ## Indicates how session fixation protection will be applied when a user authenticates. If set to "none", no protection will be applied. "newSession" will create a new empty session, with only Spring Security-related attributes migrated. "migrateSession" will create a new session and copy all session attributes to the new session. In Servlet 3.1 (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing session and use the container-supplied session fixation protection (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and newer containers, "migrateSession" in older containers. Throws an exception if "changeSessionId" is used in older containers. - attribute session-fixation-protection {"none" | "newSession" | "migrateSession" | "changeSessionId" }? -session-management.attlist &= - ## The URL to which a user will be redirected if they submit an invalid session indentifier. Typically used to detect session timeouts. - attribute invalid-session-url {xsd:token}? -session-management.attlist &= - ## Allows injection of the InvalidSessionStrategy instance used by the SessionManagementFilter - attribute invalid-session-strategy-ref {xsd:token}? -session-management.attlist &= - ## Allows injection of the SessionAuthenticationStrategy instance used by the SessionManagementFilter - attribute session-authentication-strategy-ref {xsd:token}? -session-management.attlist &= - ## Defines the URL of the error page which should be shown when the SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error code will be returned to the client. Note that this attribute doesn't apply if the error occurs during a form-based login, where the URL for authentication failure will take precedence. - attribute session-authentication-error-url {xsd:token}? - - -concurrency-control = - ## Enables concurrent session control, limiting the number of authenticated sessions a user may have at the same time. - element concurrency-control {concurrency-control.attlist, empty} - -concurrency-control.attlist &= - ## The maximum number of sessions a single authenticated user can have open at the same time. Defaults to "1". A negative value denotes unlimited sessions. - attribute max-sessions {xsd:token}? -concurrency-control.attlist &= - ## The URL a user will be redirected to if they attempt to use a session which has been "expired" because they have logged in again. - attribute expired-url {xsd:token}? -concurrency-control.attlist &= - ## Allows injection of the SessionInformationExpiredStrategy instance used by the ConcurrentSessionFilter - attribute expired-session-strategy-ref {xsd:token}? -concurrency-control.attlist &= - ## Specifies that an unauthorized error should be reported when a user attempts to login when they already have the maximum configured sessions open. The default behaviour is to expire the original session. If the session-authentication-error-url attribute is set on the session-management URL, the user will be redirected to this URL. - attribute error-if-maximum-exceeded {xsd:boolean}? -concurrency-control.attlist &= - ## Allows you to define an alias for the SessionRegistry bean in order to access it in your own configuration. - attribute session-registry-alias {xsd:token}? -concurrency-control.attlist &= - ## Allows you to define an external SessionRegistry bean to be used by the concurrency control setup. - attribute session-registry-ref {xsd:token}? - - -remember-me = - ## Sets up remember-me authentication. If used with the "key" attribute (or no attributes) the cookie-only implementation will be used. Specifying "token-repository-ref" or "remember-me-data-source-ref" will use the more secure, persisten token approach. - element remember-me {remember-me.attlist} -remember-me.attlist &= - ## The "key" used to identify cookies from a specific token-based remember-me application. You should set this to a unique value for your application. If unset, it will default to a random value generated by SecureRandom. - attribute key {xsd:token}? - -remember-me.attlist &= - (token-repository-ref | remember-me-data-source-ref | remember-me-services-ref) - -remember-me.attlist &= - user-service-ref? - -remember-me.attlist &= - ## Exports the internally defined RememberMeServices as a bean alias, allowing it to be used by other beans in the application context. - attribute services-alias {xsd:token}? - -remember-me.attlist &= - ## Determines whether the "secure" flag will be set on the remember-me cookie. If set to true, the cookie will only be submitted over HTTPS (recommended). By default, secure cookies will be used if the request is made on a secure connection. - attribute use-secure-cookie {xsd:boolean}? - -remember-me.attlist &= - ## The period (in seconds) for which the remember-me cookie should be valid. - attribute token-validity-seconds {xsd:string}? - -remember-me.attlist &= - ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful remember-me authentication. - attribute authentication-success-handler-ref {xsd:token}? -remember-me.attlist &= - ## The name of the request parameter which toggles remember-me authentication. Defaults to 'remember-me'. - attribute remember-me-parameter {xsd:token}? -remember-me.attlist &= - ## The name of cookie which store the token for remember-me authentication. Defaults to 'remember-me'. - attribute remember-me-cookie {xsd:token}? - -token-repository-ref = - ## Reference to a PersistentTokenRepository bean for use with the persistent token remember-me implementation. - attribute token-repository-ref {xsd:token} -remember-me-services-ref = - ## Allows a custom implementation of RememberMeServices to be used. Note that this implementation should return RememberMeAuthenticationToken instances with the same "key" value as specified in the remember-me element. Alternatively it should register its own AuthenticationProvider. It should also implement the LogoutHandler interface, which will be invoked when a user logs out. Typically the remember-me cookie would be removed on logout. - attribute services-ref {xsd:token}? -remember-me-data-source-ref = - ## DataSource bean for the database that contains the token repository schema. - data-source-ref - -anonymous = - ## Adds support for automatically granting all anonymous web requests a particular principal identity and a corresponding granted authority. - element anonymous {anonymous.attlist} -anonymous.attlist &= - ## The key shared between the provider and filter. This generally does not need to be set. If unset, it will default to a random value generated by SecureRandom. - attribute key {xsd:token}? -anonymous.attlist &= - ## The username that should be assigned to the anonymous request. This allows the principal to be identified, which may be important for logging and auditing. if unset, defaults to "anonymousUser". - attribute username {xsd:token}? -anonymous.attlist &= - ## The granted authority that should be assigned to the anonymous request. Commonly this is used to assign the anonymous request particular roles, which can subsequently be used in authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". - attribute granted-authority {xsd:token}? -anonymous.attlist &= - ## With the default namespace setup, the anonymous "authentication" facility is automatically enabled. You can disable it using this property. - attribute enabled {xsd:boolean}? - - -port-mappings = - ## Defines the list of mappings between http and https ports for use in redirects - element port-mappings {port-mappings.attlist, port-mapping+} - -port-mappings.attlist &= empty - -port-mapping = - ## Provides a method to map http ports to https ports when forcing a redirect. - element port-mapping {http-port, https-port} - -http-port = - ## The http port to use. - attribute http {xsd:token} - -https-port = - ## The https port to use. - attribute https {xsd:token} - - -x509 = - ## Adds support for X.509 client authentication. - element x509 {x509.attlist} -x509.attlist &= - ## The regular expression used to obtain the username from the certificate's subject. Defaults to matching on the common name using the pattern "CN=(.*?),". - attribute subject-principal-regex {xsd:token}? -x509.attlist &= - ## Explicitly specifies which user-service should be used to load user data for X.509 authenticated clients. If ommitted, the default user-service will be used. - user-service-ref? -x509.attlist &= - ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter - attribute authentication-details-source-ref {xsd:token}? - -jee = - ## Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication. - element jee {jee.attlist} -jee.attlist &= - ## A comma-separate list of roles to look for in the incoming HttpServletRequest. - attribute mappable-roles {xsd:token} -jee.attlist &= - ## Explicitly specifies which user-service should be used to load user data for container authenticated clients. If ommitted, the set of mappable-roles will be used to construct the authorities for the user. - user-service-ref? - -authentication-manager = - ## Registers the AuthenticationManager instance and allows its list of AuthenticationProviders to be defined. Also allows you to define an alias to allow you to reference the AuthenticationManager in your own beans. - element authentication-manager {authman.attlist & authentication-provider* & ldap-authentication-provider*} -authman.attlist &= - id? -authman.attlist &= - ## An alias you wish to use for the AuthenticationManager bean (not required it you are using a specific id) - attribute alias {xsd:token}? -authman.attlist &= - ## If set to true, the AuthenticationManger will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. - attribute erase-credentials {xsd:boolean}? -authman.attlist &= - ## Use this ObservationRegistry to collect metrics on various parts of the filter chain - attribute observation-registry-ref {xsd:token}? - -authentication-provider = - ## Indicates that the contained user-service should be used as an authentication source. - element authentication-provider {ap.attlist & any-user-service & password-encoder?} -ap.attlist &= - ## Specifies a reference to a separately configured AuthenticationProvider instance which should be registered within the AuthenticationManager. - ref? -ap.attlist &= - ## Specifies a reference to a separately configured UserDetailsService from which to obtain authentication data. - user-service-ref? - -user-service = - ## Creates an in-memory UserDetailsService from a properties file or a list of "user" child elements. Usernames are converted to lower-case internally to allow for case-insensitive lookups, so this should not be used if case-sensitivity is required. - element user-service {id? & (properties-file | (user*))} -properties-file = - ## The location of a Properties file where each line is in the format of username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] - attribute properties {xsd:token}? - -user = - ## Represents a user in the application. - element user {user.attlist, empty} -user.attlist &= - ## The username assigned to the user. - attribute name {xsd:token} -user.attlist &= - ## The password assigned to the user. This may be hashed if the corresponding authentication provider supports hashing (remember to set the "hash" attribute of the "user-service" element). This attribute be omitted in the case where the data will not be used for authentication, but only for accessing authorities. If omitted, the namespace will generate a random value, preventing its accidental use for authentication. Cannot be empty. - attribute password {xsd:string}? -user.attlist &= - ## One of more authorities granted to the user. Separate authorities with a comma (but no space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" - attribute authorities {xsd:token} -user.attlist &= - ## Can be set to "true" to mark an account as locked and unusable. - attribute locked {xsd:boolean}? -user.attlist &= - ## Can be set to "true" to mark an account as disabled and unusable. - attribute disabled {xsd:boolean}? - -jdbc-user-service = - ## Causes creation of a JDBC-based UserDetailsService. - element jdbc-user-service {id? & jdbc-user-service.attlist} -jdbc-user-service.attlist &= - ## The bean ID of the DataSource which provides the required tables. - attribute data-source-ref {xsd:token} -jdbc-user-service.attlist &= - cache-ref? -jdbc-user-service.attlist &= - ## An SQL statement to query a username, password, and enabled status given a username. Default is "select username,password,enabled from users where username = ?" - attribute users-by-username-query {xsd:token}? -jdbc-user-service.attlist &= - ## An SQL statement to query for a user's granted authorities given a username. The default is "select username, authority from authorities where username = ?" - attribute authorities-by-username-query {xsd:token}? -jdbc-user-service.attlist &= - ## An SQL statement to query user's group authorities given a username. The default is "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" - attribute group-authorities-by-username-query {xsd:token}? -jdbc-user-service.attlist &= - role-prefix? - -csrf = -## Element for configuration of the CsrfFilter for protection against CSRF. It also updates the default RequestCache to only replay "GET" requests. - element csrf {csrf-options.attlist} -csrf-options.attlist &= - ## Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is enabled). - attribute disabled {xsd:boolean}? -csrf-options.attlist &= - ## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" - attribute request-matcher-ref { xsd:token }? -csrf-options.attlist &= - ## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository. - attribute token-repository-ref { xsd:token }? -csrf-options.attlist &= - ## The CsrfTokenRequestHandler to use. The default is CsrfTokenRequestAttributeHandler. - attribute request-handler-ref { xsd:token }? - -headers = -## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. -element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & permissions-policy? & cross-origin-opener-policy? & cross-origin-embedder-policy? & cross-origin-resource-policy? & header*)} -headers-options.attlist &= - ## Specifies if the default headers should be disabled. Default false. - attribute defaults-disabled {xsd:token}? -headers-options.attlist &= - ## Specifies if headers should be disabled. Default false. - attribute disabled {xsd:token}? -hsts = - ## Adds support for HTTP Strict Transport Security (HSTS) - element hsts {hsts-options.attlist} -hsts-options.attlist &= - ## Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. - attribute disabled {xsd:boolean}? -hsts-options.attlist &= - ## Specifies if subdomains should be included. Default true. - attribute include-subdomains {xsd:boolean}? -hsts-options.attlist &= - ## Specifies the maximum amount of time the host should be considered a Known HSTS Host. Default one year. - attribute max-age-seconds {xsd:integer}? -hsts-options.attlist &= - ## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true. - attribute request-matcher-ref { xsd:token }? -hsts-options.attlist &= - ## Specifies if preload should be included. Default false. - attribute preload {xsd:boolean}? - -cors = -## Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is specified a HandlerMappingIntrospector is used as the CorsConfigurationSource -element cors { cors-options.attlist } -cors-options.attlist &= - ref? -cors-options.attlist &= - ## Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to use - attribute configuration-source-ref {xsd:token}? - -hpkp = - ## Adds support for HTTP Public Key Pinning (HPKP). - element hpkp {hpkp.pins,hpkp.attlist} -hpkp.pins = - ## The list with pins - element pins {hpkp.pin+} -hpkp.pin = - ## A pin is specified using the base64-encoded SPKI fingerprint as value and the cryptographic hash algorithm as attribute - element pin { - ## The cryptographic hash algorithm - attribute algorithm { xsd:string }?, - text - } -hpkp.attlist &= - ## Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. - attribute disabled {xsd:boolean}? -hpkp.attlist &= - ## Specifies if subdomains should be included. Default false. - attribute include-subdomains {xsd:boolean}? -hpkp.attlist &= - ## Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. - attribute max-age-seconds {xsd:integer}? -hpkp.attlist &= - ## Specifies if the browser should only report pin validation failures. Default true. - attribute report-only {xsd:boolean}? -hpkp.attlist &= - ## Specifies the URI to which the browser should report pin validation failures. - attribute report-uri {xsd:string}? - -content-security-policy = - ## Adds support for Content Security Policy (CSP) - element content-security-policy {csp-options.attlist} -csp-options.attlist &= - ## The security policy directive(s) for the Content-Security-Policy header or if report-only is set to true, then the Content-Security-Policy-Report-Only header is used. - attribute policy-directives {xsd:token}? -csp-options.attlist &= - ## Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy violations only. Defaults to false. - attribute report-only {xsd:boolean}? - -referrer-policy = - ## Adds support for Referrer Policy - element referrer-policy {referrer-options.attlist} -referrer-options.attlist &= - ## The policies for the Referrer-Policy header. - attribute policy {"no-referrer","no-referrer-when-downgrade","same-origin","origin","strict-origin","origin-when-cross-origin","strict-origin-when-cross-origin","unsafe-url"}? - -feature-policy = - ## Adds support for Feature Policy - element feature-policy {feature-options.attlist} -feature-options.attlist &= - ## The security policy directive(s) for the Feature-Policy header. - attribute policy-directives {xsd:token}? - -permissions-policy = - ## Adds support for Permissions Policy - element permissions-policy {permissions-options.attlist} -permissions-options.attlist &= - ## The policies for the Permissions-Policy header. - attribute policy {xsd:token}? - -cache-control = - ## Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for every request - element cache-control {cache-control.attlist} -cache-control.attlist &= - ## Specifies if Cache Control should be disabled. Default false. - attribute disabled {xsd:boolean}? - -frame-options = - ## Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options header. - element frame-options {frame-options.attlist,empty} -frame-options.attlist &= - ## If disabled, the X-Frame-Options header will not be included. Default false. - attribute disabled {xsd:boolean}? -frame-options.attlist &= - ## Specify the policy to use for the X-Frame-Options-Header. - attribute policy {"DENY","SAMEORIGIN","ALLOW-FROM"}? -frame-options.attlist &= - ## Specify the strategy to use when ALLOW-FROM is chosen. - attribute strategy {"static","whitelist","regexp"}? -frame-options.attlist &= - ## Specify a reference to the custom AllowFromStrategy to use when ALLOW-FROM is chosen. - ref? -frame-options.attlist &= - ## Specify a value to use for the chosen strategy. - attribute value {xsd:string}? -frame-options.attlist &= - ## Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' based strategy. Default is 'from'. - ## Deprecated ALLOW-FROM is an obsolete directive that no longer works in modern browsers. Instead use - ## Content-Security-Policy with the - ## frame-ancestors - ## directive. - attribute from-parameter {xsd:string}? - - -xss-protection = - ## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header. - element xss-protection {xss-protection.attlist,empty} -xss-protection.attlist &= - ## disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. - attribute disabled {xsd:boolean}? -xss-protection.attlist &= - ## Specify the value for the X-Xss-Protection header. Defaults to "0". - attribute header-value {"0"|"1"|"1; mode=block"}? - -content-type-options = - ## Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. - element content-type-options {content-type-options.attlist, empty} -content-type-options.attlist &= - ## If disabled, the X-Content-Type-Options header will not be included. Default false. - attribute disabled {xsd:boolean}? - -cross-origin-opener-policy = - ## Adds support for Cross-Origin-Opener-Policy header - element cross-origin-opener-policy {cross-origin-opener-policy-options.attlist,empty} -cross-origin-opener-policy-options.attlist &= - ## The policies for the Cross-Origin-Opener-Policy header. - attribute policy {"unsafe-none","same-origin","same-origin-allow-popups"}? - -cross-origin-embedder-policy = - ## Adds support for Cross-Origin-Embedder-Policy header - element cross-origin-embedder-policy {cross-origin-embedder-policy-options.attlist,empty} -cross-origin-embedder-policy-options.attlist &= - ## The policies for the Cross-Origin-Embedder-Policy header. - attribute policy {"unsafe-none","require-corp"}? - -cross-origin-resource-policy = - ## Adds support for Cross-Origin-Resource-Policy header - element cross-origin-resource-policy {cross-origin-resource-policy-options.attlist,empty} -cross-origin-resource-policy-options.attlist &= - ## The policies for the Cross-Origin-Resource-Policy header. - attribute policy {"cross-origin","same-origin","same-site"}? - -header= - ## Add additional headers to the response. - element header {header.attlist} -header.attlist &= - ## The name of the header to add. - attribute name {xsd:token}? -header.attlist &= - ## The value for the header. - attribute value {xsd:token}? -header.attlist &= - ## Reference to a custom HeaderWriter implementation. - ref? - -any-user-service = user-service | jdbc-user-service | ldap-user-service - -custom-filter = - ## Used to indicate that a filter bean declaration should be incorporated into the security filter chain. - element custom-filter {custom-filter.attlist} - -custom-filter.attlist &= - ref - -custom-filter.attlist &= - (after | before | position) - -after = - ## The filter immediately after which the custom-filter should be placed in the chain. This feature will only be needed by advanced users who wish to mix their own filters into the security filter chain and have some knowledge of the standard Spring Security filters. The filter names map to specific Spring Security implementation filters. - attribute after {named-security-filter} -before = - ## The filter immediately before which the custom-filter should be placed in the chain - attribute before {named-security-filter} -position = - ## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter. - attribute position {named-security-filter} - -named-security-filter = "FIRST" | "DISABLE_ENCODE_URL_FILTER" | "FORCE_EAGER_SESSION_FILTER" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "SAML2_LOGOUT_REQUEST_FILTER" | "SAML2_LOGOUT_RESPONSE_FILTER" | "CSRF_FILTER" | "SAML2_LOGOUT_FILTER" | "LOGOUT_FILTER" | "OAUTH2_AUTHORIZATION_REQUEST_FILTER" | "SAML2_AUTHENTICATION_REQUEST_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "OAUTH2_LOGIN_FILTER" | "SAML2_AUTHENTICATION_FILTER" | "FORM_LOGIN_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER" | "WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.1.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-6.1.xsd deleted file mode 100644 index f123ad830a..0000000000 --- a/config/src/main/resources/org/springframework/security/config/spring-security-6.1.xsd +++ /dev/null @@ -1,3812 +0,0 @@ - - - - - - Defines the hashing algorithm used on user passwords. Bcrypt is recommended. - - - - - - - - - - - - - Whether a string should be base64 encoded - - - - - - - - Defines the strategy use for matching incoming requests. Currently the options are 'mvc' - (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions - and 'ciRegex' for case-insensitive regular expressions. - - - - - - - - - - - - - - - - Specifies an IP port number. Used to configure an embedded LDAP server, for example. - - - - - - - - Specifies a URL. - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - - - Defines a reference to a Spring bean Id. - - - - - - - - Defines a reference to a cache for use with a UserDetailsService. - - - - - - - - A reference to a user-service (or UserDetailsService bean) Id - - - - - - - - A reference to an AuthenticationManager bean - - - - - - - - A reference to a DataSource bean - - - - - - - Enables Spring Security debugging infrastructure. This will provide human-readable - (multi-line) debugging information to monitor requests coming into the security filters. - This may include sensitive information, such as request parameters or headers, and should - only be used in a development environment. - - - - - - - - - Defines a reference to a Spring bean Id. - - - - - - Defines the hashing algorithm used on user passwords. Bcrypt is recommended. - - - - - - - - - - - - - A non-empty string prefix that will be added to role strings loaded from persistent - storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is - non-empty. - - - - - - - - Enables the use of expressions in the 'access' attributes in <intercept-url> elements - rather than the traditional list of configuration attributes. Defaults to 'true'. If - enabled, each attribute should contain a single boolean expression. If the expression - evaluates to 'true', access will be granted. - - - - - - - Defines an LDAP server location or starts an embedded server. The url indicates the - location of a remote server. If no url is given, an embedded server will be started, - listening on the supplied port number. The port is optional and defaults to 33389. A - Spring LDAP ContextSource bean will be registered for the server with the id supplied. - - - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - Specifies a URL. - - - - - - Specifies an IP port number. Used to configure an embedded LDAP server, for example. - - - - - - Username (DN) of the "manager" user identity which will be used to authenticate to a - (non-embedded) LDAP server. If omitted, anonymous access will be used. - - - - - - The password for the manager DN. This is required if the manager-dn is specified. - - - - - - Explicitly specifies an ldif file resource to load into an embedded LDAP server. The - default is classpath*:*.ldiff - - - - - - Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" - - - - - - Explicitly specifies which embedded ldap server should use. Values are 'apacheds' and - 'unboundid'. By default, it will depends if the library is available in the classpath. - - - - - - - - - - - - - - The optional server to use. If omitted, and a default LDAP server is registered (using - <ldap-server> with no Id), that server will be used. - - - - - - - - Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN - of the user. - - - - - - - - Search base for group membership searches. Defaults to "" (searching from the root). - - - - - - - - The LDAP filter used to search for users (optional). For example "(uid={0})". The - substituted parameter is the user's login name. - - - - - - - - Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. - - - - - - - - The LDAP attribute name which contains the role name which will be used within Spring - Security. Defaults to "cn". - - - - - - - - Allows the objectClass of the user entry to be specified. If set, the framework will - attempt to load standard attributes for the defined class into the returned UserDetails - object - - - - - - - - - - - - - - Allows explicit customization of the loaded user object by specifying a - UserDetailsContextMapper bean which will be called with the context information from the - user's directory entry - - - - - - - This element configures a LdapUserDetailsService which is a combination of a - FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. - - - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - The optional server to use. If omitted, and a default LDAP server is registered (using - <ldap-server> with no Id), that server will be used. - - - - - - The LDAP filter used to search for users (optional). For example "(uid={0})". The - substituted parameter is the user's login name. - - - - - - Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. - - - - - - Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN - of the user. - - - - - - Search base for group membership searches. Defaults to "" (searching from the root). - - - - - - The LDAP attribute name which contains the role name which will be used within Spring - Security. Defaults to "cn". - - - - - - Defines a reference to a cache for use with a UserDetailsService. - - - - - - A non-empty string prefix that will be added to role strings loaded from persistent - storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is - non-empty. - - - - - - Allows the objectClass of the user entry to be specified. If set, the framework will - attempt to load standard attributes for the defined class into the returned UserDetails - object - - - - - - - - - - - - Allows explicit customization of the loaded user object by specifying a - UserDetailsContextMapper bean which will be called with the context information from the - user's directory entry - - - - - - - - - The optional server to use. If omitted, and a default LDAP server is registered (using - <ldap-server> with no Id), that server will be used. - - - - - - Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. - - - - - - The LDAP filter used to search for users (optional). For example "(uid={0})". The - substituted parameter is the user's login name. - - - - - - Search base for group membership searches. Defaults to "" (searching from the root). - - - - - - Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN - of the user. - - - - - - The LDAP attribute name which contains the role name which will be used within Spring - Security. Defaults to "cn". - - - - - - A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key - "{0}" must be present and will be substituted with the username. - - - - - - A non-empty string prefix that will be added to role strings loaded from persistent - storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is - non-empty. - - - - - - Allows the objectClass of the user entry to be specified. If set, the framework will - attempt to load standard attributes for the defined class into the returned UserDetails - object - - - - - - - - - - - - Allows explicit customization of the loaded user object by specifying a - UserDetailsContextMapper bean which will be called with the context information from the - user's directory entry - - - - - - - - - The attribute in the directory which contains the user password. Defaults to - "userPassword". - - - - - - Defines the hashing algorithm used on user passwords. Bcrypt is recommended. - - - - - - - - - - - - Can be used inside a bean definition to add a security interceptor to the bean and set up - access configuration attributes for the bean's methods - - - - - - - Defines a protected method and the access control configuration attributes that apply to - it. We strongly advise you NOT to mix "protect" declarations with any services provided - "global-method-security". - - - - - - - - - - - - - - Optional AccessDecisionManager bean ID to be used by the created method security - interceptor. - - - - - - Use the AuthorizationManager API instead of AccessDecisionManager (defaults to true) - - - - - - Use this AuthorizationManager instead of the default (supercedes - use-authorization-manager) - - - - - - - - - A method name - - - - - - Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". - - - - - - - Creates a MethodSecurityMetadataSource instance - - - - - - - Defines a protected method and the access control configuration attributes that apply to - it. We strongly advise you NOT to mix "protect" declarations with any services provided - "global-method-security". - - - - - - - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - Enables the use of expressions in the 'access' attributes in <intercept-url> elements - rather than the traditional list of configuration attributes. Defaults to 'true'. If - enabled, each attribute should contain a single boolean expression. If the expression - evaluates to 'true', access will be granted. - - - - - - - Provides method security for all beans registered in the Spring application context. - Specifically, beans will be scanned for matches with Spring Security annotations. Where - there is a match, the beans will automatically be proxied and security authorization - applied to the methods accordingly. Interceptors are invoked in the order specified in - AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. - Also, annotation-based interception can be overridden by expressions listed in - <protect-pointcut> elements. - - - - - - - Defines the SecurityExpressionHandler instance which will be used if expression-based - access-control is enabled. A default implementation (with no ACL support) will be used if - not supplied. - - - - - - - - - Defines a protected pointcut and the access control configuration attributes that apply to - it. Every bean registered in the Spring application context that provides a method that - matches the pointcut will receive security authorization. - - - - - - - - - - - - - - Specifies whether the use of Spring Security's pre and post invocation annotations - (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this - application context. Defaults to "true". - - - - - - Specifies whether the use of Spring Security's @Secured annotations should be enabled for - this application context. Defaults to "false". - - - - - - Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). - This will require the javax.annotation.security classes on the classpath. Defaults to - "false". - - - - - - If true, class-based proxying will be used instead of interface-based proxying. - - - - - - If set to aspectj, then use AspectJ to intercept method invocation - - - - - - - - - - - Specifies the security context holder strategy to use, by default uses a ThreadLocal-based - strategy - - - - - - Use this ObservationRegistry to collect metrics on various parts of the filter chain - - - - - - - Provides method security for all beans registered in the Spring application context. - Specifically, beans will be scanned for matches with the ordered list of - "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a - match, the beans will automatically be proxied and security authorization applied to the - methods accordingly. If you use and enable all four sources of method security metadata - (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 - security annotations), the metadata sources will be queried in that order. In practical - terms, this enables you to use XML to override method security metadata expressed in - annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize - etc.), @Secured and finally JSR-250. - - - - - - - - Allows the default expression-based mechanism for handling Spring Security's pre and post - invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be - replace entirely. Only applies if these annotations are enabled. - - - - - - - Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and - post invocation metadata from the annotated methods. - - - - - - - - - Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the - PreInvocationAuthorizationAdviceVoter for the <pre-post-annotation-handling> element. - - - - - - - - - Customizes the PostInvocationAdviceProvider with the ref as the - PostInvocationAuthorizationAdvice for the <pre-post-annotation-handling> element. - - - - - - - - - - - - Defines the SecurityExpressionHandler instance which will be used if expression-based - access-control is enabled. A default implementation (with no ACL support) will be used if - not supplied. - - - - - - - - - - Defines a protected pointcut and the access control configuration attributes that apply to - it. Every bean registered in the Spring application context that provides a method that - matches the pointcut will receive security authorization. - - - - - - - - - Allows addition of extra AfterInvocationProvider beans which should be called by the - MethodSecurityInterceptor created by global-method-security. - - - - - - - - - - - - - - Specifies whether the use of Spring Security's pre and post invocation annotations - (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this - application context. Defaults to "disabled". - - - - - - - - - - - - Specifies whether the use of Spring Security's @Secured annotations should be enabled for - this application context. Defaults to "disabled". - - - - - - - - - - - - Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). - This will require the javax.annotation.security classes on the classpath. Defaults to - "disabled". - - - - - - - - - - - - Optional AccessDecisionManager bean ID to override the default used for method security. - - - - - - Optional RunAsmanager implementation which will be used by the configured - MethodSecurityInterceptor - - - - - - Allows the advice "order" to be set for the method security interceptor. - - - - - - If true, class based proxying will be used instead of interface based proxying. - - - - - - Can be used to specify that AspectJ should be used instead of the default Spring AOP. If - set, secured classes must be woven with the AnnotationSecurityAspect from the - spring-security-aspects module. - - - - - - - - - - - An external MethodSecurityMetadataSource instance can be supplied which will take priority - over other sources (such as the default annotations). - - - - - - A reference to an AuthenticationManager bean - - - - - - - - - - - - - - - An AspectJ expression, including the 'execution' keyword. For example, 'execution(int - com.foo.TargetObject.countLength(String))' (without the quotes). - - - - - - Access configuration attributes list that applies to all methods matching the pointcut, - e.g. "ROLE_A,ROLE_B" - - - - - - - Allows securing a Message Broker. There are two modes. If no id is specified: ensures that - any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver - registered as a custom argument resolver; ensures that the - SecurityContextChannelInterceptor is automatically registered for the - clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the - clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that - can be manually registered with the clientInboundChannel. - - - - - - - - Defines the SecurityExpressionHandler instance which will be used if expression-based - access-control is enabled. A default implementation (with no ACL support) will be used if - not supplied. - - - - - - - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. If specified, - explicit configuration within clientInboundChannel is required. If not specified, ensures - that any SimpAnnotationMethodMessageHandler has the - AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures - that the SecurityContextChannelInterceptor is automatically registered for the - clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the - clientInboundChannel. - - - - - - Disables the requirement for CSRF token to be present in the Stomp headers (default - false). Changing the default is useful if it is necessary to allow other origins to make - SockJS connections. - - - - - - Use this AuthorizationManager instead of deriving one from <intercept-message> elements - - - - - - Use AuthorizationManager API instead of SecurityMetadatasource (defaults to true) - - - - - - Use this SecurityContextHolderStrategy (note only supported in conjunction with the - AuthorizationManager API) - - - - - - - Creates an authorization rule for a websocket message. - - - - - - - - - - The destination ant pattern which will be mapped to the access attribute. For example, /** - matches any message with a destination, /admin/** matches any message that has a - destination that starts with admin. - - - - - - The access configuration attributes that apply for the configured message. For example, - permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role - 'ROLE_ADMIN'. - - - - - - The type of message to match on. Valid values are defined in SimpMessageType (i.e. - CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, - DISCONNECT_ACK, OTHER). - - - - - - - - - - - - - - - - - - - - Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created - by the namespace. - - - - - - - - - Container element for HTTP security configuration. Multiple elements can now be defined, - each with a specific pattern to which the enclosed security configuration applies. A - pattern can also be configured to bypass Spring Security's filters completely by setting - the "security" attribute to "none". - - - - - - - Specifies the access attributes and/or filter list for a particular set of URLs. - - - - - - - - - Defines the access-denied strategy that should be used. An access denied page can be - defined or a reference to an AccessDeniedHandler instance. - - - - - - - - - Sets up a form login configuration for authentication with a username and password - - - - - - - - - - - - Configures authentication support for SAML 2.0 Login - - - - - - - - - Configures SAML 2.0 Single Logout support - - - - - - - - - Adds support for X.509 client authentication. - - - - - - - - - - Adds support for basic authentication - - - - - - - - - Incorporates a logout processing filter. Most web applications require a logout filter, - although you may not require one if you write a controller to provider similar logic. - - - - - - - - - - Session-management related functionality is implemented by the addition of a - SessionManagementFilter to the filter stack. - - - - - - - Enables concurrent session control, limiting the number of authenticated sessions a user - may have at the same time. - - - - - - - - - - - - - Sets up remember-me authentication. If used with the "key" attribute (or no attributes) - the cookie-only implementation will be used. Specifying "token-repository-ref" or - "remember-me-data-source-ref" will use the more secure, persisten token approach. - - - - - - - - - Adds support for automatically granting all anonymous web requests a particular principal - identity and a corresponding granted authority. - - - - - - - - - Defines the list of mappings between http and https ports for use in redirects - - - - - - - Provides a method to map http ports to https ports when forcing a redirect. - - - - - - - - - - - - - - - Defines the SecurityExpressionHandler instance which will be used if expression-based - access-control is enabled. A default implementation (with no ACL support) will be used if - not supplied. - - - - - - - - - - - - - - - - - The request URL pattern which will be mapped to the filter chain created by this <http> - element. If omitted, the filter chain will match all requests. - - - - - - When set to 'none', requests matching the pattern attribute will be ignored by Spring - Security. No security filters will be applied and no SecurityContext will be available. If - set, the <http> element must be empty, with no children. - - - - - - - - - - - Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. - - - - - - A legacy attribute which automatically registers a login form, BASIC authentication and a - logout URL and logout services. If unspecified, defaults to "false". We'd recommend you - avoid using this and instead explicitly configure the services you require. - - - - - - Enables the use of expressions in the 'access' attributes in <intercept-url> elements - rather than the traditional list of configuration attributes. Defaults to 'true'. If - enabled, each attribute should contain a single boolean expression. If the expression - evaluates to 'true', access will be granted. - - - - - - A reference to a SecurityContextHolderStrategy bean. This can be used to customize how the - SecurityContextHolder is stored during a request - - - - - - Controls the eagerness with which an HTTP session is created by Spring Security classes. - If not set, defaults to "ifRequired". If "stateless" is used, this implies that the - application guarantees that it will not create a session. This differs from the use of - "never" which means that Spring Security will not create a session, but will make use of - one if the application does. - - - - - - - - - - - - - - A reference to a SecurityContextRepository bean. This can be used to customize how the - SecurityContext is stored between requests. - - - - - - Optional attribute that specifies that the SecurityContext should require explicit saving - rather than being synchronized from the SecurityContextHolder. Defaults to "true". - - - - - - Defines the strategy use for matching incoming requests. Currently the options are 'mvc' - (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions - and 'ciRegex' for case-insensitive regular expressions. - - - - - - - - - - - - - - Provides versions of HttpServletRequest security methods such as isUserInRole() and - getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to - "true". - - - - - - If available, runs the request as the Subject acquired from the JaasAuthenticationToken. - Defaults to "false". - - - - - - Use AuthorizationManager API instead of SecurityMetadataSource (defaults to true) - - - - - - Use this AuthorizationManager instead of deriving one from <intercept-url> elements - - - - - - Optional attribute specifying the ID of the AccessDecisionManager implementation which - should be used for authorizing HTTP requests. - - - - - - Optional attribute specifying the realm name that will be used for all authentication - features that require a realm name (eg BASIC and Digest authentication). If unspecified, - defaults to "Spring Security Application". - - - - - - Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. - - - - - - Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults - to "false" - - - - - - Corresponds to the shouldFilterAllDispatcherTypes property of AuthorizationFilter. Do not - work when use-authorization-manager=false. Defaults to "true". - - - - - - Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" - (rewriting is disabled). - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - A reference to an AuthenticationManager bean - - - - - - Use this ObservationRegistry to collect metrics on various parts of the filter chain - - - - - - - - - Defines a reference to a Spring bean Id. - - - - - - The access denied page that an authenticated user will be redirected to if they request a - page which they don't have the authority to access. - - - - - - - - The access denied page that an authenticated user will be redirected to if they request a - page which they don't have the authority to access. - - - - - - - - - The request URL pattern which will be mapped to the FilterChain. - - - - - - Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. - - - - - - The access configuration attributes that apply for the configured path. - - - - - - The HTTP Method for which the access configuration attributes should apply. If not - specified, the attributes will apply to any method. - - - - - - - - - - - - - - - - - - Used to specify that a URL must be accessed over http or https, or that there is no - preference. The value should be "http", "https" or "any", respectively. - - - - - - The path to the servlet. This attribute is only applicable when 'request-matcher' is - 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are - 2 or more HttpServlet's registered in the ServletContext that have mappings starting with - '/' and are different; 2) The pattern starts with the same value of a registered - HttpServlet path, excluding the default (root) HttpServlet '/'. - - - - - - - - - Specifies the URL that will cause a logout. Spring Security will initialize a filter that - responds to this particular URL. Defaults to /logout if unspecified. - - - - - - Specifies the URL to display once the user has logged out. If not specified, defaults to - <form-login-login-page>/?logout (i.e. /login?logout). - - - - - - Specifies whether a logout also causes HttpSession invalidation, which is generally - desirable. If unspecified, defaults to true. - - - - - - A reference to a LogoutSuccessHandler implementation which will be used to determine the - destination to which the user is taken after logging out. - - - - - - A comma-separated list of the names of cookies which should be deleted when the user logs - out - - - - - - - Allow the RequestCache used for saving requests during the login process to be set - - - - - - - - - - - The URL that the login form is posted to. If unspecified, it defaults to /login. - - - - - - The name of the request parameter which contains the username. Defaults to 'username'. - - - - - - The name of the request parameter which contains the password. Defaults to 'password'. - - - - - - The URL that will be redirected to after successful authentication, if the user's previous - action could not be resumed. This generally happens if the user visits a login page - without having first requested a secured operation that triggers authentication. If - unspecified, defaults to the root of the application. - - - - - - Whether the user should always be redirected to the default-target-url after login. - - - - - - The URL for the login page. If no login URL is specified, Spring Security will - automatically create a login URL at GET /login and a corresponding filter to render that - login URL when requested. - - - - - - The URL for the login failure page. If no login failure URL is specified, Spring Security - will automatically create a failure login URL at /login?error and a corresponding filter - to render that login failure URL when requested. - - - - - - Reference to an AuthenticationSuccessHandler bean which should be used to handle a - successful authentication request. Should not be used in combination with - default-target-url (or always-use-default-target-url) as the implementation should always - deal with navigation to the subsequent destination - - - - - - Reference to an AuthenticationFailureHandler bean which should be used to handle a failed - authentication request. Should not be used in combination with authentication-failure-url - as the implementation should always deal with navigation to the subsequent destination - - - - - - Reference to an AuthenticationDetailsSource which will be used by the authentication - filter - - - - - - The URL for the ForwardAuthenticationFailureHandler - - - - - - The URL for the ForwardAuthenticationSuccessHandler - - - - - - - Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. - - - - - - - - - - Reference to the ClientRegistrationRepository - - - - - - Reference to the OAuth2AuthorizedClientRepository - - - - - - Reference to the OAuth2AuthorizedClientService - - - - - - Reference to the AuthorizationRequestRepository - - - - - - Reference to the OAuth2AuthorizationRequestResolver - - - - - - Reference to the authorization RedirectStrategy - - - - - - Reference to the OAuth2AccessTokenResponseClient - - - - - - Reference to the GrantedAuthoritiesMapper - - - - - - Reference to the OAuth2UserService - - - - - - Reference to the OpenID Connect OAuth2UserService - - - - - - The URI where the filter processes authentication requests - - - - - - The URI to send users to login - - - - - - Reference to the AuthenticationSuccessHandler - - - - - - Reference to the AuthenticationFailureHandler - - - - - - Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider - - - - - - - Configures OAuth 2.0 Client support. - - - - - - - - - - - - - Reference to the ClientRegistrationRepository - - - - - - Reference to the OAuth2AuthorizedClientRepository - - - - - - Reference to the OAuth2AuthorizedClientService - - - - - - - Configures OAuth 2.0 Authorization Code Grant. - - - - - - - - - - Reference to the AuthorizationRequestRepository - - - - - - Reference to the authorization RedirectStrategy - - - - - - Reference to the OAuth2AuthorizationRequestResolver - - - - - - Reference to the OAuth2AccessTokenResponseClient - - - - - - - Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 - Provider. - - - - - - - - - - - - Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. - - - - - - - - - - The ID that uniquely identifies the client registration. - - - - - - The client identifier. - - - - - - The client secret. - - - - - - The method used to authenticate the client with the provider. The supported values are - client_secret_basic, client_secret_post and none (public clients). - - - - - - - - - - - - - - - The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The - supported values are authorization_code, client_credentials and password. - - - - - - - - - - - - - The client’s registered redirect URI that the Authorization Server redirects the - end-user’s user-agent to after the end-user has authenticated and authorized access to the - client. - - - - - - A comma-separated list of scope(s) requested by the client during the Authorization - Request flow, such as openid, email, or profile. - - - - - - A descriptive name used for the client. The name may be used in certain scenarios, such as - when displaying the name of the client in the auto-generated login page. - - - - - - A reference to the associated provider. May reference a 'provider' element or use one of - the common providers (google, github, facebook, okta). - - - - - - - The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. - - - - - - - - - - The ID that uniquely identifies the provider. - - - - - - The Authorization Endpoint URI for the Authorization Server. - - - - - - The Token Endpoint URI for the Authorization Server. - - - - - - The UserInfo Endpoint URI used to access the claims/attributes of the authenticated - end-user. - - - - - - The authentication method used when sending the access token to the UserInfo Endpoint. The - supported values are header, form and query. - - - - - - - - - - - - - The name of the attribute returned in the UserInfo Response that references the Name or - Identifier of the end-user. - - - - - - The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server, which - contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID - Token and optionally the UserInfo Response. - - - - - - The URI used to discover the configuration information for an OAuth 2.0 or OpenID Connect - 1.0 Provider. - - - - - - - Configures authentication support as an OAuth 2.0 Resource Server. - - - - - - - - - - - - - - Reference to an AuthenticationManagerResolver - - - - - - Reference to a BearerTokenResolver - - - - - - Reference to a AuthenticationEntryPoint - - - - - - - Configures JWT authentication - - - - - - - - - - The URI to use to collect the JWK Set for verifying JWTs - - - - - - Reference to a JwtDecoder - - - - - - Reference to a Converter<Jwt, AbstractAuthenticationToken> - - - - - - - Configuration Opaque Token authentication - - - - - - - - - - The URI to use to introspect opaque token attributes - - - - - - The Client ID to use to authenticate the introspection request - - - - - - The Client secret to use to authenticate the introspection request - - - - - - Reference to an OpaqueTokenIntrospector - - - - - - Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful - introspection result into an Authentication. - - - - - - - - - Reference to the RelyingPartyRegistrationRepository - - - - - - Reference to the Saml2AuthenticationRequestRepository - - - - - - Reference to the Saml2AuthenticationRequestResolver - - - - - - Reference to the AuthenticationConverter - - - - - - The URI where the filter processes authentication requests - - - - - - The URI to send users to login - - - - - - Reference to the AuthenticationSuccessHandler - - - - - - Reference to the AuthenticationFailureHandler - - - - - - Reference to the AuthenticationManager - - - - - - - - - The URL by which the relying or asserting party can trigger logout - - - - - - The URL by which the asserting party can send a SAML 2.0 Logout Request - - - - - - The URL by which the asserting party can send a SAML 2.0 Logout Response - - - - - - Reference to the RelyingPartyRegistrationRepository - - - - - - Reference to the Saml2LogoutRequestValidator - - - - - - Reference to the Saml2LogoutRequestResolver - - - - - - Reference to the Saml2LogoutRequestRepository - - - - - - Reference to the Saml2LogoutResponseValidator - - - - - - Reference to the Saml2LogoutResponseResolver - - - - - - - Container element for relying party(ies) registered with a SAML 2.0 identity provider - - - - - - - - - - - - Represents a relying party registered with a SAML 2.0 identity provider - - - - - - - - - - - - - - The ID that uniquely identifies the relying party registration. - - - - - - The location of the Identity Provider's metadata. - - - - - - The relying party's EntityID - - - - - - The Assertion Consumer Service Location - - - - - - The Assertion Consumer Service Binding - - - - - - A reference to the associated asserting party. - - - - - - The relying party <a - href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService - Location</a> - - - - - - The relying party <a - href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService - Response Location</a> - - - - - - The relying party <a - href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService - Binding</a> - - - - - - - The relying party's signing credential - - - - - - - - - - The private key location - - - - - - The certificate location - - - - - - - The relying party's decryption credential - - - - - - - - - - The private key location - - - - - - The certificate location - - - - - - - The configuration metadata of the Asserting party - - - - - - - - - - - - - - A unique identifier of the asserting party. - - - - - - The asserting party's EntityID. - - - - - - Indicates the asserting party's preference that relying parties should sign the - AuthnRequest before sending - - - - - - The <a - href="https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint">SingleSignOnService</a> - Location. - - - - - - The <a - href="https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint">SingleSignOnService</a> - Binding. - - - - - - A comma separated list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this - asserting party, in preference order. - - - - - - The asserting party <a - href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService - Location</a> - - - - - - The asserting party <a - href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService - Response Location</a> - - - - - - The asserting party <a - href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService - Binding</a> - - - - - - - The relying party's verification credential - - - - - - - - - - The private key location - - - - - - The certificate location - - - - - - - The asserting party's encryption credential - - - - - - - - - - The private key location - - - - - - The certificate location - - - - - - - Used to explicitly configure a FilterChainProxy instance with a FilterChainMap - - - - - - - - - - - - - Defines the strategy use for matching incoming requests. Currently the options are 'mvc' - (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions - and 'ciRegex' for case-insensitive regular expressions. - - - - - - - - - - - - - - - Used within to define a specific URL pattern and the list of filters which apply to the - URLs matching that pattern. When multiple filter-chain elements are assembled in a list in - order to configure a FilterChainProxy, the most specific patterns must be placed at the - top of the list, with most general ones at the bottom. - - - - - - - - - - The request URL pattern which will be mapped to the FilterChain. - - - - - - Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. - - - - - - A comma separated list of bean names that implement Filter that should be processed for - this FilterChain. If the value is none, then no Filters will be used for this FilterChain. - - - - - - - - The request URL pattern which will be mapped to the FilterChain. - - - - - - - - Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. - - - - - - - Used to explicitly configure a FilterSecurityMetadataSource bean for use with a - FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy - explicitly, rather than using the <http> element. The intercept-url elements used should - only contain pattern, method and access attributes. Any others will result in a - configuration error. - - - - - - - Specifies the access attributes and/or filter list for a particular set of URLs. - - - - - - - - - - - - - - Enables the use of expressions in the 'access' attributes in <intercept-url> elements - rather than the traditional list of configuration attributes. Defaults to 'true'. If - enabled, each attribute should contain a single boolean expression. If the expression - evaluates to 'true', access will be granted. - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - Defines the strategy use for matching incoming requests. Currently the options are 'mvc' - (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions - and 'ciRegex' for case-insensitive regular expressions. - - - - - - - - - - - - - - - - - Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. - - - - - - Reference to an AuthenticationDetailsSource which will be used by the authentication - filter - - - - - - - Adds support for the password management. - - - - - - - - - - The change password page. Defaults to "/change-password". - - - - - - - - - Specifies that SessionAuthenticationStrategy must be explicitly invoked. Default false - (i.e. SessionManagementFilter will implicitly invoke SessionAuthenticationStrategy). - - - - - - Indicates how session fixation protection will be applied when a user authenticates. If - set to "none", no protection will be applied. "newSession" will create a new empty - session, with only Spring Security-related attributes migrated. "migrateSession" will - create a new session and copy all session attributes to the new session. In Servlet 3.1 - (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing - session and use the container-supplied session fixation protection - (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and - newer containers, "migrateSession" in older containers. Throws an exception if - "changeSessionId" is used in older containers. - - - - - - - - - - - - - - The URL to which a user will be redirected if they submit an invalid session indentifier. - Typically used to detect session timeouts. - - - - - - Allows injection of the InvalidSessionStrategy instance used by the - SessionManagementFilter - - - - - - Allows injection of the SessionAuthenticationStrategy instance used by the - SessionManagementFilter - - - - - - Defines the URL of the error page which should be shown when the - SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error - code will be returned to the client. Note that this attribute doesn't apply if the error - occurs during a form-based login, where the URL for authentication failure will take - precedence. - - - - - - - - - The maximum number of sessions a single authenticated user can have open at the same time. - Defaults to "1". A negative value denotes unlimited sessions. - - - - - - The URL a user will be redirected to if they attempt to use a session which has been - "expired" because they have logged in again. - - - - - - Allows injection of the SessionInformationExpiredStrategy instance used by the - ConcurrentSessionFilter - - - - - - Specifies that an unauthorized error should be reported when a user attempts to login when - they already have the maximum configured sessions open. The default behaviour is to expire - the original session. If the session-authentication-error-url attribute is set on the - session-management URL, the user will be redirected to this URL. - - - - - - Allows you to define an alias for the SessionRegistry bean in order to access it in your - own configuration. - - - - - - Allows you to define an external SessionRegistry bean to be used by the concurrency - control setup. - - - - - - - - - The "key" used to identify cookies from a specific token-based remember-me application. - You should set this to a unique value for your application. If unset, it will default to a - random value generated by SecureRandom. - - - - - - Reference to a PersistentTokenRepository bean for use with the persistent token - remember-me implementation. - - - - - - A reference to a DataSource bean - - - - - - - A reference to a user-service (or UserDetailsService bean) Id - - - - - - Exports the internally defined RememberMeServices as a bean alias, allowing it to be used - by other beans in the application context. - - - - - - Determines whether the "secure" flag will be set on the remember-me cookie. If set to - true, the cookie will only be submitted over HTTPS (recommended). By default, secure - cookies will be used if the request is made on a secure connection. - - - - - - The period (in seconds) for which the remember-me cookie should be valid. - - - - - - Reference to an AuthenticationSuccessHandler bean which should be used to handle a - successful remember-me authentication. - - - - - - The name of the request parameter which toggles remember-me authentication. Defaults to - 'remember-me'. - - - - - - The name of cookie which store the token for remember-me authentication. Defaults to - 'remember-me'. - - - - - - - - Reference to a PersistentTokenRepository bean for use with the persistent token - remember-me implementation. - - - - - - - - Allows a custom implementation of RememberMeServices to be used. Note that this - implementation should return RememberMeAuthenticationToken instances with the same "key" - value as specified in the remember-me element. Alternatively it should register its own - AuthenticationProvider. It should also implement the LogoutHandler interface, which will - be invoked when a user logs out. Typically the remember-me cookie would be removed on - logout. - - - - - - - - - - - - The key shared between the provider and filter. This generally does not need to be set. If - unset, it will default to a random value generated by SecureRandom. - - - - - - The username that should be assigned to the anonymous request. This allows the principal - to be identified, which may be important for logging and auditing. if unset, defaults to - "anonymousUser". - - - - - - The granted authority that should be assigned to the anonymous request. Commonly this is - used to assign the anonymous request particular roles, which can subsequently be used in - authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". - - - - - - With the default namespace setup, the anonymous "authentication" facility is automatically - enabled. You can disable it using this property. - - - - - - - - - - The http port to use. - - - - - - - - The https port to use. - - - - - - - - - The regular expression used to obtain the username from the certificate's subject. - Defaults to matching on the common name using the pattern "CN=(.*?),". - - - - - - A reference to a user-service (or UserDetailsService bean) Id - - - - - - Reference to an AuthenticationDetailsSource which will be used by the authentication - filter - - - - - - - Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration - with container authentication. - - - - - - - - - - A comma-separate list of roles to look for in the incoming HttpServletRequest. - - - - - - A reference to a user-service (or UserDetailsService bean) Id - - - - - - - Registers the AuthenticationManager instance and allows its list of - AuthenticationProviders to be defined. Also allows you to define an alias to allow you to - reference the AuthenticationManager in your own beans. - - - - - - - Indicates that the contained user-service should be used as an authentication source. - - - - - - - - element which defines a password encoding strategy. Used by an authentication provider to - convert submitted passwords to hashed versions, for example. - - - - - - - - - - - - - Sets up an ldap authentication provider - - - - - - - Specifies that an LDAP provider should use an LDAP compare operation of the user's - password to authenticate the user - - - - - - - element which defines a password encoding strategy. Used by an authentication provider to - convert submitted passwords to hashed versions, for example. - - - - - - - - - - - - - - - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - An alias you wish to use for the AuthenticationManager bean (not required it you are using - a specific id) - - - - - - If set to true, the AuthenticationManger will attempt to clear any credentials data in the - returned Authentication object, once the user has been authenticated. - - - - - - Use this ObservationRegistry to collect metrics on various parts of the filter chain - - - - - - - - - Defines a reference to a Spring bean Id. - - - - - - A reference to a user-service (or UserDetailsService bean) Id - - - - - - - Creates an in-memory UserDetailsService from a properties file or a list of "user" child - elements. Usernames are converted to lower-case internally to allow for case-insensitive - lookups, so this should not be used if case-sensitivity is required. - - - - - - - Represents a user in the application. - - - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - - - - - The location of a Properties file where each line is in the format of - username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] - - - - - - - - - The username assigned to the user. - - - - - - The password assigned to the user. This may be hashed if the corresponding authentication - provider supports hashing (remember to set the "hash" attribute of the "user-service" - element). This attribute be omitted in the case where the data will not be used for - authentication, but only for accessing authorities. If omitted, the namespace will - generate a random value, preventing its accidental use for authentication. Cannot be - empty. - - - - - - One of more authorities granted to the user. Separate authorities with a comma (but no - space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" - - - - - - Can be set to "true" to mark an account as locked and unusable. - - - - - - Can be set to "true" to mark an account as disabled and unusable. - - - - - - - Causes creation of a JDBC-based UserDetailsService. - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - - - - - The bean ID of the DataSource which provides the required tables. - - - - - - Defines a reference to a cache for use with a UserDetailsService. - - - - - - An SQL statement to query a username, password, and enabled status given a username. - Default is "select username,password,enabled from users where username = ?" - - - - - - An SQL statement to query for a user's granted authorities given a username. The default - is "select username, authority from authorities where username = ?" - - - - - - An SQL statement to query user's group authorities given a username. The default is - "select g.id, g.group_name, ga.authority from groups g, group_members gm, - group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" - - - - - - A non-empty string prefix that will be added to role strings loaded from persistent - storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is - non-empty. - - - - - - - Element for configuration of the CsrfFilter for protection against CSRF. It also updates - the default RequestCache to only replay "GET" requests. - - - - - - - - - - Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is - enabled). - - - - - - The RequestMatcher instance to be used to determine if CSRF should be applied. Default is - any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" - - - - - - The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by - LazyCsrfTokenRepository. - - - - - - The CsrfTokenRequestHandler to use. The default is CsrfTokenRequestAttributeHandler. - - - - - - - Element for configuration of the HeaderWritersFilter. Enables easy setting for the - X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. - - - - - - - - - - - - - - - - - - - - - - - - - - Specifies if the default headers should be disabled. Default false. - - - - - - Specifies if headers should be disabled. Default false. - - - - - - - Adds support for HTTP Strict Transport Security (HSTS) - - - - - - - - - - Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. - - - - - - Specifies if subdomains should be included. Default true. - - - - - - Specifies the maximum amount of time the host should be considered a Known HSTS Host. - Default one year. - - - - - - The RequestMatcher instance to be used to determine if the header should be set. Default - is if HttpServletRequest.isSecure() is true. - - - - - - Specifies if preload should be included. Default false. - - - - - - - Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is - specified a HandlerMappingIntrospector is used as the CorsConfigurationSource - - - - - - - - - - Defines a reference to a Spring bean Id. - - - - - - Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to - use - - - - - - - Adds support for HTTP Public Key Pinning (HPKP). - - - - - - - - - - - - - - - - - - The list with pins - - - - - - - - - - - A pin is specified using the base64-encoded SPKI fingerprint as value and the - cryptographic hash algorithm as attribute - - - - - - The cryptographic hash algorithm - - - - - - - - - Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. - - - - - - Specifies if subdomains should be included. Default false. - - - - - - Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. - - - - - - Specifies if the browser should only report pin validation failures. Default true. - - - - - - Specifies the URI to which the browser should report pin validation failures. - - - - - - - Adds support for Content Security Policy (CSP) - - - - - - - - - - The security policy directive(s) for the Content-Security-Policy header or if report-only - is set to true, then the Content-Security-Policy-Report-Only header is used. - - - - - - Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy - violations only. Defaults to false. - - - - - - - Adds support for Referrer Policy - - - - - - - - - - The policies for the Referrer-Policy header. - - - - - - - - - - - - - - - - - - - Adds support for Feature Policy - - - - - - - - - - The security policy directive(s) for the Feature-Policy header. - - - - - - - Adds support for Permissions Policy - - - - - - - - - - The policies for the Permissions-Policy header. - - - - - - - Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for - every request - - - - - - - - - - Specifies if Cache Control should be disabled. Default false. - - - - - - - Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options - header. - - - - - - - - - - If disabled, the X-Frame-Options header will not be included. Default false. - - - - - - Specify the policy to use for the X-Frame-Options-Header. - - - - - - - - - - - - - Specify the strategy to use when ALLOW-FROM is chosen. - - - - - - - - - - - - - Defines a reference to a Spring bean Id. - - - - - - Specify a value to use for the chosen strategy. - - - - - - Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' - based strategy. Default is 'from'. Deprecated ALLOW-FROM is an obsolete directive that no - longer works in modern browsers. Instead use Content-Security-Policy with the <a - href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors">frame-ancestors</a> - directive. - - - - - - - Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the - X-XSS-Protection header. - - - - - - - - - - disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. - - - - - - Specify the value for the X-Xss-Protection header. Defaults to "0". - - - - - - - - - - - - - - Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. - - - - - - - - - - If disabled, the X-Content-Type-Options header will not be included. Default false. - - - - - - - Adds support for Cross-Origin-Opener-Policy header - - - - - - - - - - The policies for the Cross-Origin-Opener-Policy header. - - - - - - - - - - - - - - Adds support for Cross-Origin-Embedder-Policy header - - - - - - - - - - The policies for the Cross-Origin-Embedder-Policy header. - - - - - - - - - - - - - Adds support for Cross-Origin-Resource-Policy header - - - - - - - - - - The policies for the Cross-Origin-Resource-Policy header. - - - - - - - - - - - - - - Add additional headers to the response. - - - - - - - - - - The name of the header to add. - - - - - - The value for the header. - - - - - - Defines a reference to a Spring bean Id. - - - - - - - - Used to indicate that a filter bean declaration should be incorporated into the security - filter chain. - - - - - - - - - - - The filter immediately after which the custom-filter should be placed in the chain. This - feature will only be needed by advanced users who wish to mix their own filters into the - security filter chain and have some knowledge of the standard Spring Security filters. The - filter names map to specific Spring Security implementation filters. - - - - - - The filter immediately before which the custom-filter should be placed in the chain - - - - - - The explicit position at which the custom-filter should be placed in the chain. Use if you - are replacing a standard filter. - - - - - - - - The filter immediately after which the custom-filter should be placed in the chain. This - feature will only be needed by advanced users who wish to mix their own filters into the - security filter chain and have some knowledge of the standard Spring Security filters. The - filter names map to specific Spring Security implementation filters. - - - - - - - - The filter immediately before which the custom-filter should be placed in the chain - - - - - - - - The explicit position at which the custom-filter should be placed in the chain. Use if you - are replacing a standard filter. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.2.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-6.2.rnc deleted file mode 100644 index 7f89ced5af..0000000000 --- a/config/src/main/resources/org/springframework/security/config/spring-security-6.2.rnc +++ /dev/null @@ -1,1346 +0,0 @@ -namespace a = "https://relaxng.org/ns/compatibility/annotations/1.0" -datatypes xsd = "http://www.w3.org/2001/XMLSchema-datatypes" - -default namespace = "http://www.springframework.org/schema/security" - -start = http | ldap-server | authentication-provider | ldap-authentication-provider | any-user-service | ldap-server | ldap-authentication-provider - -hash = - ## Defines the hashing algorithm used on user passwords. Bcrypt is recommended. - attribute hash {"bcrypt"} -base64 = - ## Whether a string should be base64 encoded - attribute base64 {xsd:boolean} -request-matcher = - ## Defines the strategy use for matching incoming requests. Currently the options are 'mvc' (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for case-insensitive regular expressions. - attribute request-matcher {"mvc" | "ant" | "regex" | "ciRegex"} -port = - ## Specifies an IP port number. Used to configure an embedded LDAP server, for example. - attribute port { xsd:nonNegativeInteger } -url = - ## Specifies a URL. - attribute url { xsd:token } -id = - ## A bean identifier, used for referring to the bean elsewhere in the context. - attribute id {xsd:token} -name = - ## A bean identifier, used for referring to the bean elsewhere in the context. - attribute name {xsd:token} -ref = - ## Defines a reference to a Spring bean Id. - attribute ref {xsd:token} - -cache-ref = - ## Defines a reference to a cache for use with a UserDetailsService. - attribute cache-ref {xsd:token} - -user-service-ref = - ## A reference to a user-service (or UserDetailsService bean) Id - attribute user-service-ref {xsd:token} - -authentication-manager-ref = - ## A reference to an AuthenticationManager bean - attribute authentication-manager-ref {xsd:token} - -data-source-ref = - ## A reference to a DataSource bean - attribute data-source-ref {xsd:token} - - - -debug = - ## Enables Spring Security debugging infrastructure. This will provide human-readable (multi-line) debugging information to monitor requests coming into the security filters. This may include sensitive information, such as request parameters or headers, and should only be used in a development environment. - element debug {empty} - -password-encoder = - ## element which defines a password encoding strategy. Used by an authentication provider to convert submitted passwords to hashed versions, for example. - element password-encoder {password-encoder.attlist} -password-encoder.attlist &= - ref | (hash) - -role-prefix = - ## A non-empty string prefix that will be added to role strings loaded from persistent storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is non-empty. - attribute role-prefix {xsd:token} - -use-expressions = - ## Enables the use of expressions in the 'access' attributes in elements rather than the traditional list of configuration attributes. Defaults to 'true'. If enabled, each attribute should contain a single boolean expression. If the expression evaluates to 'true', access will be granted. - attribute use-expressions {xsd:boolean} - -ldap-server = - ## Defines an LDAP server location or starts an embedded server. The url indicates the location of a remote server. If no url is given, an embedded server will be started, listening on the supplied port number. The port is optional and defaults to 33389. A Spring LDAP ContextSource bean will be registered for the server with the id supplied. - element ldap-server {ldap-server.attlist} -ldap-server.attlist &= id? -ldap-server.attlist &= (url | port)? -ldap-server.attlist &= - ## Username (DN) of the "manager" user identity which will be used to authenticate to a (non-embedded) LDAP server. If omitted, anonymous access will be used. - attribute manager-dn {xsd:string}? -ldap-server.attlist &= - ## The password for the manager DN. This is required if the manager-dn is specified. - attribute manager-password {xsd:string}? -ldap-server.attlist &= - ## Explicitly specifies an ldif file resource to load into an embedded LDAP server. The default is classpath*:*.ldiff - attribute ldif { xsd:string }? -ldap-server.attlist &= - ## Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" - attribute root { xsd:string }? -ldap-server.attlist &= - ## Explicitly specifies which embedded ldap server should use. Values are 'apacheds' and 'unboundid'. By default, it will depends if the library is available in the classpath. - attribute mode { "apacheds" | "unboundid" }? - -ldap-server-ref-attribute = - ## The optional server to use. If omitted, and a default LDAP server is registered (using with no Id), that server will be used. - attribute server-ref {xsd:token} - - -group-search-filter-attribute = - ## Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN of the user. - attribute group-search-filter {xsd:token} -group-search-base-attribute = - ## Search base for group membership searches. Defaults to "" (searching from the root). - attribute group-search-base {xsd:token} -user-search-filter-attribute = - ## The LDAP filter used to search for users (optional). For example "(uid={0})". The substituted parameter is the user's login name. - attribute user-search-filter {xsd:token} -user-search-base-attribute = - ## Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. - attribute user-search-base {xsd:token} -group-role-attribute-attribute = - ## The LDAP attribute name which contains the role name which will be used within Spring Security. Defaults to "cn". - attribute group-role-attribute {xsd:token} -user-details-class-attribute = - ## Allows the objectClass of the user entry to be specified. If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object - attribute user-details-class {"person" | "inetOrgPerson"} -user-context-mapper-attribute = - ## Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry - attribute user-context-mapper-ref {xsd:token} - - -ldap-user-service = - ## This element configures a LdapUserDetailsService which is a combination of a FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. - element ldap-user-service {ldap-us.attlist} -ldap-us.attlist &= id? -ldap-us.attlist &= - ldap-server-ref-attribute? -ldap-us.attlist &= - user-search-filter-attribute? -ldap-us.attlist &= - user-search-base-attribute? -ldap-us.attlist &= - group-search-filter-attribute? -ldap-us.attlist &= - group-search-base-attribute? -ldap-us.attlist &= - group-role-attribute-attribute? -ldap-us.attlist &= - cache-ref? -ldap-us.attlist &= - role-prefix? -ldap-us.attlist &= - (user-details-class-attribute | user-context-mapper-attribute)? - -ldap-authentication-provider = - ## Sets up an ldap authentication provider - element ldap-authentication-provider {ldap-ap.attlist, password-compare-element?} -ldap-ap.attlist &= - ldap-server-ref-attribute? -ldap-ap.attlist &= - user-search-base-attribute? -ldap-ap.attlist &= - user-search-filter-attribute? -ldap-ap.attlist &= - group-search-base-attribute? -ldap-ap.attlist &= - group-search-filter-attribute? -ldap-ap.attlist &= - group-role-attribute-attribute? -ldap-ap.attlist &= - ## A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key "{0}" must be present and will be substituted with the username. - attribute user-dn-pattern {xsd:token}? -ldap-ap.attlist &= - role-prefix? -ldap-ap.attlist &= - (user-details-class-attribute | user-context-mapper-attribute)? - -password-compare-element = - ## Specifies that an LDAP provider should use an LDAP compare operation of the user's password to authenticate the user - element password-compare {password-compare.attlist, password-encoder?} - -password-compare.attlist &= - ## The attribute in the directory which contains the user password. Defaults to "userPassword". - attribute password-attribute {xsd:token}? -password-compare.attlist &= - hash? - -intercept-methods = - ## Can be used inside a bean definition to add a security interceptor to the bean and set up access configuration attributes for the bean's methods - element intercept-methods {intercept-methods.attlist, protect+} -intercept-methods.attlist &= - ## Optional AccessDecisionManager bean ID to be used by the created method security interceptor. - attribute access-decision-manager-ref {xsd:token}? -intercept-methods.attlist &= - ## Use the AuthorizationManager API instead of AccessDecisionManager (defaults to true) - attribute use-authorization-manager {xsd:boolean}? -intercept-methods.attlist &= - ## Use this AuthorizationManager instead of the default (supercedes use-authorization-manager) - attribute authorization-manager-ref {xsd:token}? - -protect = - ## Defines a protected method and the access control configuration attributes that apply to it. We strongly advise you NOT to mix "protect" declarations with any services provided "global-method-security". - element protect {protect.attlist, empty} -protect.attlist &= - ## A method name - attribute method {xsd:token} -protect.attlist &= - ## Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". - attribute access {xsd:token} - -method-security-metadata-source = - ## Creates a MethodSecurityMetadataSource instance - element method-security-metadata-source {msmds.attlist, protect+} -msmds.attlist &= id? - -msmds.attlist &= use-expressions? - -method-security = - ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with Spring Security annotations. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. Interceptors are invoked in the order specified in AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. Also, annotation-based interception can be overridden by expressions listed in elements. - element method-security {method-security.attlist, expression-handler?, protect-pointcut*} -method-security.attlist &= - ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "true". - attribute pre-post-enabled {xsd:boolean}? -method-security.attlist &= - ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "false". - attribute secured-enabled {xsd:boolean}? -method-security.attlist &= - ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "false". - attribute jsr250-enabled {xsd:boolean}? -method-security.attlist &= - ## If true, class-based proxying will be used instead of interface-based proxying. - attribute proxy-target-class {xsd:boolean}? -method-security.attlist &= - ## If set to aspectj, then use AspectJ to intercept method invocation - attribute mode {"aspectj"}? -method-security.attlist &= - ## Specifies the security context holder strategy to use, by default uses a ThreadLocal-based strategy - attribute security-context-holder-strategy-ref {xsd:string}? -method-security.attlist &= - ## Use this ObservationRegistry to collect metrics on various parts of the filter chain - attribute observation-registry-ref {xsd:token}? - -global-method-security = - ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with the ordered list of "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. If you use and enable all four sources of method security metadata (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 security annotations), the metadata sources will be queried in that order. In practical terms, this enables you to use XML to override method security metadata expressed in annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize etc.), @Secured and finally JSR-250. - element global-method-security {global-method-security.attlist, (pre-post-annotation-handling | expression-handler)?, protect-pointcut*, after-invocation-provider*} -global-method-security.attlist &= - ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "disabled". - attribute pre-post-annotations {"disabled" | "enabled" }? -global-method-security.attlist &= - ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "disabled". - attribute secured-annotations {"disabled" | "enabled" }? -global-method-security.attlist &= - ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "disabled". - attribute jsr250-annotations {"disabled" | "enabled" }? -global-method-security.attlist &= - ## Optional AccessDecisionManager bean ID to override the default used for method security. - attribute access-decision-manager-ref {xsd:token}? -global-method-security.attlist &= - ## Optional RunAsmanager implementation which will be used by the configured MethodSecurityInterceptor - attribute run-as-manager-ref {xsd:token}? -global-method-security.attlist &= - ## Allows the advice "order" to be set for the method security interceptor. - attribute order {xsd:token}? -global-method-security.attlist &= - ## If true, class based proxying will be used instead of interface based proxying. - attribute proxy-target-class {xsd:boolean}? -global-method-security.attlist &= - ## Can be used to specify that AspectJ should be used instead of the default Spring AOP. If set, secured classes must be woven with the AnnotationSecurityAspect from the spring-security-aspects module. - attribute mode {"aspectj"}? -global-method-security.attlist &= - ## An external MethodSecurityMetadataSource instance can be supplied which will take priority over other sources (such as the default annotations). - attribute metadata-source-ref {xsd:token}? -global-method-security.attlist &= - authentication-manager-ref? - - -after-invocation-provider = - ## Allows addition of extra AfterInvocationProvider beans which should be called by the MethodSecurityInterceptor created by global-method-security. - element after-invocation-provider {ref} - -pre-post-annotation-handling = - ## Allows the default expression-based mechanism for handling Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be replace entirely. Only applies if these annotations are enabled. - element pre-post-annotation-handling {invocation-attribute-factory, pre-invocation-advice, post-invocation-advice} - -invocation-attribute-factory = - ## Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and post invocation metadata from the annotated methods. - element invocation-attribute-factory {ref} - -pre-invocation-advice = - ## Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the PreInvocationAuthorizationAdviceVoter for the element. - element pre-invocation-advice {ref} - -post-invocation-advice = - ## Customizes the PostInvocationAdviceProvider with the ref as the PostInvocationAuthorizationAdvice for the element. - element post-invocation-advice {ref} - - -expression-handler = - ## Defines the SecurityExpressionHandler instance which will be used if expression-based access-control is enabled. A default implementation (with no ACL support) will be used if not supplied. - element expression-handler {ref} - -protect-pointcut = - ## Defines a protected pointcut and the access control configuration attributes that apply to it. Every bean registered in the Spring application context that provides a method that matches the pointcut will receive security authorization. - element protect-pointcut {protect-pointcut.attlist, empty} -protect-pointcut.attlist &= - ## An AspectJ expression, including the 'execution' keyword. For example, 'execution(int com.foo.TargetObject.countLength(String))' (without the quotes). - attribute expression {xsd:string} -protect-pointcut.attlist &= - ## Access configuration attributes list that applies to all methods matching the pointcut, e.g. "ROLE_A,ROLE_B" - attribute access {xsd:token} - -websocket-message-broker = - ## Allows securing a Message Broker. There are two modes. If no id is specified: ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that can be manually registered with the clientInboundChannel. - element websocket-message-broker { websocket-message-broker.attrlist, (intercept-message* & expression-handler?) } - -websocket-message-broker.attrlist &= - ## A bean identifier, used for referring to the bean elsewhere in the context. If specified, explicit configuration within clientInboundChannel is required. If not specified, ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. - attribute id {xsd:token}? -websocket-message-broker.attrlist &= - ## Disables the requirement for CSRF token to be present in the Stomp headers (default false). Changing the default is useful if it is necessary to allow other origins to make SockJS connections. - attribute same-origin-disabled {xsd:boolean}? -websocket-message-broker.attrlist &= - ## Use this AuthorizationManager instead of deriving one from elements - attribute authorization-manager-ref {xsd:string}? -websocket-message-broker.attrlist &= - ## Use AuthorizationManager API instead of SecurityMetadatasource (defaults to true) - attribute use-authorization-manager {xsd:boolean}? -websocket-message-broker.attrlist &= - ## Use this SecurityContextHolderStrategy (note only supported in conjunction with the AuthorizationManager API) - attribute security-context-holder-strategy-ref {xsd:string}? - -intercept-message = - ## Creates an authorization rule for a websocket message. - element intercept-message {intercept-message.attrlist} - -intercept-message.attrlist &= - ## The destination ant pattern which will be mapped to the access attribute. For example, /** matches any message with a destination, /admin/** matches any message that has a destination that starts with admin. - attribute pattern {xsd:token}? -intercept-message.attrlist &= - ## The access configuration attributes that apply for the configured message. For example, permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role 'ROLE_ADMIN'. - attribute access {xsd:token}? -intercept-message.attrlist &= - ## The type of message to match on. Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER). - attribute type {"CONNECT" | "CONNECT_ACK" | "HEARTBEAT" | "MESSAGE" | "SUBSCRIBE"| "UNSUBSCRIBE" | "DISCONNECT" | "DISCONNECT_ACK" | "OTHER"}? - -http-firewall = - ## Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created by the namespace. - element http-firewall {ref} - -http = - ## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "security" attribute to "none". - element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & oauth2-login? & oauth2-client? & oauth2-resource-server? & saml2-login? & saml2-logout? & x509? & jee? & http-basic? & logout? & password-management? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) } -http.attlist &= - ## The request URL pattern which will be mapped to the filter chain created by this element. If omitted, the filter chain will match all requests. - attribute pattern {xsd:token}? -http.attlist &= - ## When set to 'none', requests matching the pattern attribute will be ignored by Spring Security. No security filters will be applied and no SecurityContext will be available. If set, the element must be empty, with no children. - attribute security {"none"}? -http.attlist &= - ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. - attribute request-matcher-ref { xsd:token }? -http.attlist &= - ## A legacy attribute which automatically registers a login form, BASIC authentication and a logout URL and logout services. If unspecified, defaults to "false". We'd recommend you avoid using this and instead explicitly configure the services you require. - attribute auto-config {xsd:boolean}? -http.attlist &= - use-expressions? -http.attlist &= - ## A reference to a SecurityContextHolderStrategy bean. This can be used to customize how the SecurityContextHolder is stored during a request - attribute security-context-holder-strategy-ref {xsd:token}? -http.attlist &= - ## Controls the eagerness with which an HTTP session is created by Spring Security classes. If not set, defaults to "ifRequired". If "stateless" is used, this implies that the application guarantees that it will not create a session. This differs from the use of "never" which means that Spring Security will not create a session, but will make use of one if the application does. - attribute create-session {"ifRequired" | "always" | "never" | "stateless"}? -http.attlist &= - ## A reference to a SecurityContextRepository bean. This can be used to customize how the SecurityContext is stored between requests. - attribute security-context-repository-ref {xsd:token}? -http.attlist &= - ## Optional attribute that specifies that the SecurityContext should require explicit saving rather than being synchronized from the SecurityContextHolder. Defaults to "true". - attribute security-context-explicit-save {xsd:boolean}? -http.attlist &= - request-matcher? -http.attlist &= - ## Provides versions of HttpServletRequest security methods such as isUserInRole() and getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to "true". - attribute servlet-api-provision {xsd:boolean}? -http.attlist &= - ## If available, runs the request as the Subject acquired from the JaasAuthenticationToken. Defaults to "false". - attribute jaas-api-provision {xsd:boolean}? -http.attlist &= - ## Use AuthorizationManager API instead of SecurityMetadataSource (defaults to true) - attribute use-authorization-manager {xsd:boolean}? -http.attlist &= - ## Use this AuthorizationManager instead of deriving one from elements - attribute authorization-manager-ref {xsd:token}? -http.attlist &= - ## Optional attribute specifying the ID of the AccessDecisionManager implementation which should be used for authorizing HTTP requests. - attribute access-decision-manager-ref {xsd:token}? -http.attlist &= - ## Optional attribute specifying the realm name that will be used for all authentication features that require a realm name (eg BASIC and Digest authentication). If unspecified, defaults to "Spring Security Application". - attribute realm {xsd:token}? -http.attlist &= - ## Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. - attribute entry-point-ref {xsd:token}? -http.attlist &= - ## Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults to "false" - attribute once-per-request {xsd:boolean}? -http.attlist &= - ## Corresponds to the shouldFilterAllDispatcherTypes property of AuthorizationFilter. Do not work when use-authorization-manager=false. Defaults to "true". - attribute filter-all-dispatcher-types {xsd:boolean}? -http.attlist &= - ## Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" (rewriting is disabled). - attribute disable-url-rewriting {xsd:boolean}? -http.attlist &= - ## Exposes the list of filters defined by this configuration under this bean name in the application context. - name? -http.attlist &= - authentication-manager-ref? -http.attlist &= - ## Use this ObservationRegistry to collect metrics on various parts of the filter chain - attribute observation-registry-ref {xsd:token}? - -access-denied-handler = - ## Defines the access-denied strategy that should be used. An access denied page can be defined or a reference to an AccessDeniedHandler instance. - element access-denied-handler {access-denied-handler.attlist, empty} -access-denied-handler.attlist &= (ref | access-denied-handler-page) - -access-denied-handler-page = - ## The access denied page that an authenticated user will be redirected to if they request a page which they don't have the authority to access. - attribute error-page {xsd:token} - -intercept-url = - ## Specifies the access attributes and/or filter list for a particular set of URLs. - element intercept-url {intercept-url.attlist, empty} -intercept-url.attlist &= - (pattern | request-matcher-ref) -intercept-url.attlist &= - ## The access configuration attributes that apply for the configured path. - attribute access {xsd:token}? -intercept-url.attlist &= - ## The HTTP Method for which the access configuration attributes should apply. If not specified, the attributes will apply to any method. - attribute method {"GET" | "DELETE" | "HEAD" | "OPTIONS" | "POST" | "PUT" | "PATCH" | "TRACE"}? - -intercept-url.attlist &= - ## Used to specify that a URL must be accessed over http or https, or that there is no preference. The value should be "http", "https" or "any", respectively. - attribute requires-channel {xsd:token}? -intercept-url.attlist &= - ## The path to the servlet. This attribute is only applicable when 'request-matcher' is 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are 2 or more HttpServlet's registered in the ServletContext that have mappings starting with '/' and are different; 2) The pattern starts with the same value of a registered HttpServlet path, excluding the default (root) HttpServlet '/'. - attribute servlet-path {xsd:token}? - -logout = - ## Incorporates a logout processing filter. Most web applications require a logout filter, although you may not require one if you write a controller to provider similar logic. - element logout {logout.attlist, empty} -logout.attlist &= - ## Specifies the URL that will cause a logout. Spring Security will initialize a filter that responds to this particular URL. Defaults to /logout if unspecified. - attribute logout-url {xsd:token}? -logout.attlist &= - ## Specifies the URL to display once the user has logged out. If not specified, defaults to /?logout (i.e. /login?logout). - attribute logout-success-url {xsd:token}? -logout.attlist &= - ## Specifies whether a logout also causes HttpSession invalidation, which is generally desirable. If unspecified, defaults to true. - attribute invalidate-session {xsd:boolean}? -logout.attlist &= - ## A reference to a LogoutSuccessHandler implementation which will be used to determine the destination to which the user is taken after logging out. - attribute success-handler-ref {xsd:token}? -logout.attlist &= - ## A comma-separated list of the names of cookies which should be deleted when the user logs out - attribute delete-cookies {xsd:token}? - -request-cache = - ## Allow the RequestCache used for saving requests during the login process to be set - element request-cache {ref} - -form-login = - ## Sets up a form login configuration for authentication with a username and password - element form-login {form-login.attlist, empty} -form-login.attlist &= - ## The URL that the login form is posted to. If unspecified, it defaults to /login. - attribute login-processing-url {xsd:token}? -form-login.attlist &= - ## The name of the request parameter which contains the username. Defaults to 'username'. - attribute username-parameter {xsd:token}? -form-login.attlist &= - ## The name of the request parameter which contains the password. Defaults to 'password'. - attribute password-parameter {xsd:token}? -form-login.attlist &= - ## The URL that will be redirected to after successful authentication, if the user's previous action could not be resumed. This generally happens if the user visits a login page without having first requested a secured operation that triggers authentication. If unspecified, defaults to the root of the application. - attribute default-target-url {xsd:token}? -form-login.attlist &= - ## Whether the user should always be redirected to the default-target-url after login. - attribute always-use-default-target {xsd:boolean}? -form-login.attlist &= - ## The URL for the login page. If no login URL is specified, Spring Security will automatically create a login URL at GET /login and a corresponding filter to render that login URL when requested. - attribute login-page {xsd:token}? -form-login.attlist &= - ## The URL for the login failure page. If no login failure URL is specified, Spring Security will automatically create a failure login URL at /login?error and a corresponding filter to render that login failure URL when requested. - attribute authentication-failure-url {xsd:token}? -form-login.attlist &= - ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful authentication request. Should not be used in combination with default-target-url (or always-use-default-target-url) as the implementation should always deal with navigation to the subsequent destination - attribute authentication-success-handler-ref {xsd:token}? -form-login.attlist &= - ## Reference to an AuthenticationFailureHandler bean which should be used to handle a failed authentication request. Should not be used in combination with authentication-failure-url as the implementation should always deal with navigation to the subsequent destination - attribute authentication-failure-handler-ref {xsd:token}? -form-login.attlist &= - ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter - attribute authentication-details-source-ref {xsd:token}? -form-login.attlist &= - ## The URL for the ForwardAuthenticationFailureHandler - attribute authentication-failure-forward-url {xsd:token}? -form-login.attlist &= - ## The URL for the ForwardAuthenticationSuccessHandler - attribute authentication-success-forward-url {xsd:token}? - -oauth2-login = - ## Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. - element oauth2-login {oauth2-login.attlist} -oauth2-login.attlist &= - ## Reference to the ClientRegistrationRepository - attribute client-registration-repository-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the OAuth2AuthorizedClientRepository - attribute authorized-client-repository-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the OAuth2AuthorizedClientService - attribute authorized-client-service-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the AuthorizationRequestRepository - attribute authorization-request-repository-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the OAuth2AuthorizationRequestResolver - attribute authorization-request-resolver-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the authorization RedirectStrategy - attribute authorization-redirect-strategy-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the OAuth2AccessTokenResponseClient - attribute access-token-response-client-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the GrantedAuthoritiesMapper - attribute user-authorities-mapper-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the OAuth2UserService - attribute user-service-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the OpenID Connect OAuth2UserService - attribute oidc-user-service-ref {xsd:token}? -oauth2-login.attlist &= - ## The URI where the filter processes authentication requests - attribute login-processing-url {xsd:token}? -oauth2-login.attlist &= - ## The URI to send users to login - attribute login-page {xsd:token}? -oauth2-login.attlist &= - ## Reference to the AuthenticationSuccessHandler - attribute authentication-success-handler-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the AuthenticationFailureHandler - attribute authentication-failure-handler-ref {xsd:token}? -oauth2-login.attlist &= - ## Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider - attribute jwt-decoder-factory-ref {xsd:token}? - -oauth2-client = - ## Configures OAuth 2.0 Client support. - element oauth2-client {oauth2-client.attlist, (authorization-code-grant?) } -oauth2-client.attlist &= - ## Reference to the ClientRegistrationRepository - attribute client-registration-repository-ref {xsd:token}? -oauth2-client.attlist &= - ## Reference to the OAuth2AuthorizedClientRepository - attribute authorized-client-repository-ref {xsd:token}? -oauth2-client.attlist &= - ## Reference to the OAuth2AuthorizedClientService - attribute authorized-client-service-ref {xsd:token}? - -authorization-code-grant = - ## Configures OAuth 2.0 Authorization Code Grant. - element authorization-code-grant {authorization-code-grant.attlist, empty} -authorization-code-grant.attlist &= - ## Reference to the AuthorizationRequestRepository - attribute authorization-request-repository-ref {xsd:token}? -authorization-code-grant.attlist &= - ## Reference to the authorization RedirectStrategy - attribute authorization-redirect-strategy-ref {xsd:token}? -authorization-code-grant.attlist &= - ## Reference to the OAuth2AuthorizationRequestResolver - attribute authorization-request-resolver-ref {xsd:token}? -authorization-code-grant.attlist &= - ## Reference to the OAuth2AccessTokenResponseClient - attribute access-token-response-client-ref {xsd:token}? - -client-registrations = - ## Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. - element client-registrations {client-registration+, provider*} - -client-registration = - ## Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. - element client-registration {client-registration.attlist} -client-registration.attlist &= - ## The ID that uniquely identifies the client registration. - attribute registration-id {xsd:token} -client-registration.attlist &= - ## The client identifier. - attribute client-id {xsd:token} -client-registration.attlist &= - ## The client secret. - attribute client-secret {xsd:token}? -client-registration.attlist &= - ## The method used to authenticate the client with the provider. The supported values are client_secret_basic, client_secret_post and none (public clients). - attribute client-authentication-method {"client_secret_basic" | "basic" | "client_secret_post" | "post" | "none"}? -client-registration.attlist &= - ## The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The supported values are authorization_code, client_credentials and password. - attribute authorization-grant-type {"authorization_code" | "client_credentials" | "password"}? -client-registration.attlist &= - ## The client’s registered redirect URI that the Authorization Server redirects the end-user’s user-agent to after the end-user has authenticated and authorized access to the client. - attribute redirect-uri {xsd:token}? -client-registration.attlist &= - ## A comma-separated list of scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. - attribute scope {xsd:token}? -client-registration.attlist &= - ## A descriptive name used for the client. The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. - attribute client-name {xsd:token}? -client-registration.attlist &= - ## A reference to the associated provider. May reference a 'provider' element or use one of the common providers (google, github, facebook, okta). - attribute provider-id {xsd:token} - -provider = - ## The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. - element provider {provider.attlist} -provider.attlist &= - ## The ID that uniquely identifies the provider. - attribute provider-id {xsd:token} -provider.attlist &= - ## The Authorization Endpoint URI for the Authorization Server. - attribute authorization-uri {xsd:token}? -provider.attlist &= - ## The Token Endpoint URI for the Authorization Server. - attribute token-uri {xsd:token}? -provider.attlist &= - ## The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. - attribute user-info-uri {xsd:token}? -provider.attlist &= - ## The authentication method used when sending the access token to the UserInfo Endpoint. The supported values are header, form and query. - attribute user-info-authentication-method {"header" | "form" | "query"}? -provider.attlist &= - ## The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. - attribute user-info-user-name-attribute {xsd:token}? -provider.attlist &= - ## The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server, which contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID Token and optionally the UserInfo Response. - attribute jwk-set-uri {xsd:token}? -provider.attlist &= - ## The URI used to discover the configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. - attribute issuer-uri {xsd:token}? - -oauth2-resource-server = - ## Configures authentication support as an OAuth 2.0 Resource Server. - element oauth2-resource-server {oauth2-resource-server.attlist, (jwt? & opaque-token?)} -oauth2-resource-server.attlist &= - ## Reference to an AuthenticationManagerResolver - attribute authentication-manager-resolver-ref {xsd:token}? -oauth2-resource-server.attlist &= - ## Reference to a BearerTokenResolver - attribute bearer-token-resolver-ref {xsd:token}? -oauth2-resource-server.attlist &= - ## Reference to a AuthenticationEntryPoint - attribute entry-point-ref {xsd:token}? - -jwt = - ## Configures JWT authentication - element jwt {jwt.attlist} -jwt.attlist &= - ## The URI to use to collect the JWK Set for verifying JWTs - attribute jwk-set-uri {xsd:token}? -jwt.attlist &= - ## Reference to a JwtDecoder - attribute decoder-ref {xsd:token}? -jwt.attlist &= - ## Reference to a Converter - attribute jwt-authentication-converter-ref {xsd:token}? - -opaque-token = - ## Configuration Opaque Token authentication - element opaque-token {opaque-token.attlist} -opaque-token.attlist &= - ## The URI to use to introspect opaque token attributes - attribute introspection-uri {xsd:token}? -opaque-token.attlist &= - ## The Client ID to use to authenticate the introspection request - attribute client-id {xsd:token}? -opaque-token.attlist &= - ## The Client secret to use to authenticate the introspection request - attribute client-secret {xsd:token}? -opaque-token.attlist &= - ## Reference to an OpaqueTokenIntrospector - attribute introspector-ref {xsd:token}? -opaque-token.attlist &= - ## Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful introspection result into an Authentication. - attribute authentication-converter-ref {xsd:token}? - -saml2-login = - ## Configures authentication support for SAML 2.0 Login - element saml2-login {saml2-login.attlist} -saml2-login.attlist &= - ## Reference to the RelyingPartyRegistrationRepository - attribute relying-party-registration-repository-ref {xsd:token}? -saml2-login.attlist &= - ## Reference to the Saml2AuthenticationRequestRepository - attribute authentication-request-repository-ref {xsd:token}? -saml2-login.attlist &= - ## Reference to the Saml2AuthenticationRequestResolver - attribute authentication-request-resolver-ref {xsd:token}? -saml2-login.attlist &= - ## Reference to the AuthenticationConverter - attribute authentication-converter-ref {xsd:token}? -saml2-login.attlist &= - ## The URI where the filter processes authentication requests - attribute login-processing-url {xsd:token}? -saml2-login.attlist &= - ## The URI to send users to login - attribute login-page {xsd:token}? -saml2-login.attlist &= - ## Reference to the AuthenticationSuccessHandler - attribute authentication-success-handler-ref {xsd:token}? -saml2-login.attlist &= - ## Reference to the AuthenticationFailureHandler - attribute authentication-failure-handler-ref {xsd:token}? -saml2-login.attlist &= - ## Reference to the AuthenticationManager - attribute authentication-manager-ref {xsd:token}? - -saml2-logout = - ## Configures SAML 2.0 Single Logout support - element saml2-logout {saml2-logout.attlist} -saml2-logout.attlist &= - ## The URL by which the relying or asserting party can trigger logout - attribute logout-url {xsd:token}? -saml2-logout.attlist &= - ## The URL by which the asserting party can send a SAML 2.0 Logout Request - attribute logout-request-url {xsd:token}? -saml2-logout.attlist &= - ## The URL by which the asserting party can send a SAML 2.0 Logout Response - attribute logout-response-url {xsd:token}? -saml2-logout.attlist &= - ## Reference to the RelyingPartyRegistrationRepository - attribute relying-party-registration-repository-ref {xsd:token}? -saml2-logout.attlist &= - ## Reference to the Saml2LogoutRequestValidator - attribute logout-request-validator-ref {xsd:token}? -saml2-logout.attlist &= - ## Reference to the Saml2LogoutRequestResolver - attribute logout-request-resolver-ref {xsd:token}? -saml2-logout.attlist &= - ## Reference to the Saml2LogoutRequestRepository - attribute logout-request-repository-ref {xsd:token}? -saml2-logout.attlist &= - ## Reference to the Saml2LogoutResponseValidator - attribute logout-response-validator-ref {xsd:token}? -saml2-logout.attlist &= - ## Reference to the Saml2LogoutResponseResolver - attribute logout-response-resolver-ref {xsd:token}? - -relying-party-registrations = - ## Container element for relying party(ies) registered with a SAML 2.0 identity provider - element relying-party-registrations {relying-party-registration+, asserting-party*} - -relying-party-registration = - ## Represents a relying party registered with a SAML 2.0 identity provider - element relying-party-registration {relying-party-registration.attlist, signing-credential*, decryption-credential*} -relying-party-registration.attlist &= - ## The ID that uniquely identifies the relying party registration. - attribute registration-id {xsd:token} -relying-party-registration.attlist &= - ## The location of the Identity Provider's metadata. - attribute metadata-location {xsd:token}? -relying-party-registration.attlist &= - ## The relying party's EntityID - attribute entity-id {xsd:token}? -relying-party-registration.attlist &= - ## The Assertion Consumer Service Location - attribute assertion-consumer-service-location {xsd:token}? -relying-party-registration.attlist &= - ## The Assertion Consumer Service Binding - attribute assertion-consumer-service-binding {xsd:token}? -relying-party-registration.attlist &= - ## A reference to the associated asserting party. - attribute asserting-party-id {xsd:token}? -relying-party-registration.attlist &= - ## The relying party SingleLogoutService Location - attribute single-logout-service-location {xsd:token}? -relying-party-registration.attlist &= - ## The relying party SingleLogoutService Response Location - attribute single-logout-service-response-location {xsd:token}? -relying-party-registration.attlist &= - ## The relying party SingleLogoutService Binding - attribute single-logout-service-binding {xsd:token}? - -signing-credential = - ## The relying party's signing credential - element signing-credential {signing-credential.attlist} -signing-credential.attlist &= - ## The private key location - attribute private-key-location {xsd:token} -signing-credential.attlist &= - ## The certificate location - attribute certificate-location {xsd:token} - -decryption-credential = - ## The relying party's decryption credential - element decryption-credential {decryption-credential.attlist} -decryption-credential.attlist &= - ## The private key location - attribute private-key-location {xsd:token} -decryption-credential.attlist &= - ## The certificate location - attribute certificate-location {xsd:token} - -asserting-party = - ## The configuration metadata of the Asserting party - element asserting-party {asserting-party.attlist, verification-credential*, encryption-credential*} -asserting-party.attlist &= - ## A unique identifier of the asserting party. - attribute asserting-party-id {xsd:token} -asserting-party.attlist &= - ## The asserting party's EntityID. - attribute entity-id {xsd:token} -asserting-party.attlist &= - ## Indicates the asserting party's preference that relying parties should sign the AuthnRequest before sending - attribute want-authn-requests-signed {xsd:token}? -asserting-party.attlist &= - ## The SingleSignOnService Location. - attribute single-sign-on-service-location {xsd:token} -asserting-party.attlist &= - ## The SingleSignOnService Binding. - attribute single-sign-on-service-binding {xsd:token}? -asserting-party.attlist &= - ## A comma separated list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this asserting party, in preference order. - attribute signing-algorithms {xsd:token}? -asserting-party.attlist &= - ## The asserting party SingleLogoutService Location - attribute single-logout-service-location {xsd:token}? -asserting-party.attlist &= - ## The asserting party SingleLogoutService Response Location - attribute single-logout-service-response-location {xsd:token}? -asserting-party.attlist &= - ## The asserting party SingleLogoutService Binding - attribute single-logout-service-binding {xsd:token}? - -verification-credential = - ## The relying party's verification credential - element verification-credential {verification-credential.attlist} -verification-credential.attlist &= - ## The private key location - attribute private-key-location {xsd:token} -verification-credential.attlist &= - ## The certificate location - attribute certificate-location {xsd:token} - -encryption-credential = - ## The asserting party's encryption credential - element encryption-credential {encryption-credential.attlist} -encryption-credential.attlist &= - ## The private key location - attribute private-key-location {xsd:token} -encryption-credential.attlist &= - ## The certificate location - attribute certificate-location {xsd:token} - - -filter-chain-map = - ## Used to explicitly configure a FilterChainProxy instance with a FilterChainMap - element filter-chain-map {filter-chain-map.attlist, filter-chain+} -filter-chain-map.attlist &= - request-matcher? - -filter-chain = - ## Used within to define a specific URL pattern and the list of filters which apply to the URLs matching that pattern. When multiple filter-chain elements are assembled in a list in order to configure a FilterChainProxy, the most specific patterns must be placed at the top of the list, with most general ones at the bottom. - element filter-chain {filter-chain.attlist, empty} -filter-chain.attlist &= - (pattern | request-matcher-ref) -filter-chain.attlist &= - ## A comma separated list of bean names that implement Filter that should be processed for this FilterChain. If the value is none, then no Filters will be used for this FilterChain. - attribute filters {xsd:token} - -pattern = - ## The request URL pattern which will be mapped to the FilterChain. - attribute pattern {xsd:token} -request-matcher-ref = - ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. - attribute request-matcher-ref {xsd:token} - -filter-security-metadata-source = - ## Used to explicitly configure a FilterSecurityMetadataSource bean for use with a FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy explicitly, rather than using the element. The intercept-url elements used should only contain pattern, method and access attributes. Any others will result in a configuration error. - element filter-security-metadata-source {fsmds.attlist, intercept-url+} -fsmds.attlist &= - use-expressions? -fsmds.attlist &= - id? -fsmds.attlist &= - request-matcher? - -http-basic = - ## Adds support for basic authentication - element http-basic {http-basic.attlist, empty} - -http-basic.attlist &= - ## Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. - attribute entry-point-ref {xsd:token}? -http-basic.attlist &= - ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter - attribute authentication-details-source-ref {xsd:token}? - -password-management = - ## Adds support for the password management. - element password-management {password-management.attlist, empty} - -password-management.attlist &= - ## The change password page. Defaults to "/change-password". - attribute change-password-page {xsd:string}? - -session-management = - ## Session-management related functionality is implemented by the addition of a SessionManagementFilter to the filter stack. - element session-management {session-management.attlist, concurrency-control?} - -session-management.attlist &= - ## Specifies that SessionAuthenticationStrategy must be explicitly invoked. Default false (i.e. SessionManagementFilter will implicitly invoke SessionAuthenticationStrategy). - attribute authentication-strategy-explicit-invocation {xsd:boolean}? -session-management.attlist &= - ## Indicates how session fixation protection will be applied when a user authenticates. If set to "none", no protection will be applied. "newSession" will create a new empty session, with only Spring Security-related attributes migrated. "migrateSession" will create a new session and copy all session attributes to the new session. In Servlet 3.1 (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing session and use the container-supplied session fixation protection (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and newer containers, "migrateSession" in older containers. Throws an exception if "changeSessionId" is used in older containers. - attribute session-fixation-protection {"none" | "newSession" | "migrateSession" | "changeSessionId" }? -session-management.attlist &= - ## The URL to which a user will be redirected if they submit an invalid session indentifier. Typically used to detect session timeouts. - attribute invalid-session-url {xsd:token}? -session-management.attlist &= - ## Allows injection of the InvalidSessionStrategy instance used by the SessionManagementFilter - attribute invalid-session-strategy-ref {xsd:token}? -session-management.attlist &= - ## Allows injection of the SessionAuthenticationStrategy instance used by the SessionManagementFilter - attribute session-authentication-strategy-ref {xsd:token}? -session-management.attlist &= - ## Defines the URL of the error page which should be shown when the SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error code will be returned to the client. Note that this attribute doesn't apply if the error occurs during a form-based login, where the URL for authentication failure will take precedence. - attribute session-authentication-error-url {xsd:token}? - - -concurrency-control = - ## Enables concurrent session control, limiting the number of authenticated sessions a user may have at the same time. - element concurrency-control {concurrency-control.attlist, empty} - -concurrency-control.attlist &= - ## The maximum number of sessions a single authenticated user can have open at the same time. Defaults to "1". A negative value denotes unlimited sessions. - attribute max-sessions {xsd:token}? -concurrency-control.attlist &= - ## The URL a user will be redirected to if they attempt to use a session which has been "expired" because they have logged in again. - attribute expired-url {xsd:token}? -concurrency-control.attlist &= - ## Allows injection of the SessionInformationExpiredStrategy instance used by the ConcurrentSessionFilter - attribute expired-session-strategy-ref {xsd:token}? -concurrency-control.attlist &= - ## Specifies that an unauthorized error should be reported when a user attempts to login when they already have the maximum configured sessions open. The default behaviour is to expire the original session. If the session-authentication-error-url attribute is set on the session-management URL, the user will be redirected to this URL. - attribute error-if-maximum-exceeded {xsd:boolean}? -concurrency-control.attlist &= - ## Allows you to define an alias for the SessionRegistry bean in order to access it in your own configuration. - attribute session-registry-alias {xsd:token}? -concurrency-control.attlist &= - ## Allows you to define an external SessionRegistry bean to be used by the concurrency control setup. - attribute session-registry-ref {xsd:token}? - - -remember-me = - ## Sets up remember-me authentication. If used with the "key" attribute (or no attributes) the cookie-only implementation will be used. Specifying "token-repository-ref" or "remember-me-data-source-ref" will use the more secure, persisten token approach. - element remember-me {remember-me.attlist} -remember-me.attlist &= - ## The "key" used to identify cookies from a specific token-based remember-me application. You should set this to a unique value for your application. If unset, it will default to a random value generated by SecureRandom. - attribute key {xsd:token}? - -remember-me.attlist &= - (token-repository-ref | remember-me-data-source-ref | remember-me-services-ref) - -remember-me.attlist &= - user-service-ref? - -remember-me.attlist &= - ## Exports the internally defined RememberMeServices as a bean alias, allowing it to be used by other beans in the application context. - attribute services-alias {xsd:token}? - -remember-me.attlist &= - ## Determines whether the "secure" flag will be set on the remember-me cookie. If set to true, the cookie will only be submitted over HTTPS (recommended). By default, secure cookies will be used if the request is made on a secure connection. - attribute use-secure-cookie {xsd:boolean}? - -remember-me.attlist &= - ## The period (in seconds) for which the remember-me cookie should be valid. - attribute token-validity-seconds {xsd:string}? - -remember-me.attlist &= - ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful remember-me authentication. - attribute authentication-success-handler-ref {xsd:token}? -remember-me.attlist &= - ## The name of the request parameter which toggles remember-me authentication. Defaults to 'remember-me'. - attribute remember-me-parameter {xsd:token}? -remember-me.attlist &= - ## The name of cookie which store the token for remember-me authentication. Defaults to 'remember-me'. - attribute remember-me-cookie {xsd:token}? - -token-repository-ref = - ## Reference to a PersistentTokenRepository bean for use with the persistent token remember-me implementation. - attribute token-repository-ref {xsd:token} -remember-me-services-ref = - ## Allows a custom implementation of RememberMeServices to be used. Note that this implementation should return RememberMeAuthenticationToken instances with the same "key" value as specified in the remember-me element. Alternatively it should register its own AuthenticationProvider. It should also implement the LogoutHandler interface, which will be invoked when a user logs out. Typically the remember-me cookie would be removed on logout. - attribute services-ref {xsd:token}? -remember-me-data-source-ref = - ## DataSource bean for the database that contains the token repository schema. - data-source-ref - -anonymous = - ## Adds support for automatically granting all anonymous web requests a particular principal identity and a corresponding granted authority. - element anonymous {anonymous.attlist} -anonymous.attlist &= - ## The key shared between the provider and filter. This generally does not need to be set. If unset, it will default to a random value generated by SecureRandom. - attribute key {xsd:token}? -anonymous.attlist &= - ## The username that should be assigned to the anonymous request. This allows the principal to be identified, which may be important for logging and auditing. if unset, defaults to "anonymousUser". - attribute username {xsd:token}? -anonymous.attlist &= - ## The granted authority that should be assigned to the anonymous request. Commonly this is used to assign the anonymous request particular roles, which can subsequently be used in authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". - attribute granted-authority {xsd:token}? -anonymous.attlist &= - ## With the default namespace setup, the anonymous "authentication" facility is automatically enabled. You can disable it using this property. - attribute enabled {xsd:boolean}? - - -port-mappings = - ## Defines the list of mappings between http and https ports for use in redirects - element port-mappings {port-mappings.attlist, port-mapping+} - -port-mappings.attlist &= empty - -port-mapping = - ## Provides a method to map http ports to https ports when forcing a redirect. - element port-mapping {http-port, https-port} - -http-port = - ## The http port to use. - attribute http {xsd:token} - -https-port = - ## The https port to use. - attribute https {xsd:token} - - -x509 = - ## Adds support for X.509 client authentication. - element x509 {x509.attlist} -x509.attlist &= - ## The regular expression used to obtain the username from the certificate's subject. Defaults to matching on the common name using the pattern "CN=(.*?),". - attribute subject-principal-regex {xsd:token}? -x509.attlist &= - ## Explicitly specifies which user-service should be used to load user data for X.509 authenticated clients. If ommitted, the default user-service will be used. - user-service-ref? -x509.attlist &= - ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter - attribute authentication-details-source-ref {xsd:token}? - -jee = - ## Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication. - element jee {jee.attlist} -jee.attlist &= - ## A comma-separate list of roles to look for in the incoming HttpServletRequest. - attribute mappable-roles {xsd:token} -jee.attlist &= - ## Explicitly specifies which user-service should be used to load user data for container authenticated clients. If ommitted, the set of mappable-roles will be used to construct the authorities for the user. - user-service-ref? - -authentication-manager = - ## Registers the AuthenticationManager instance and allows its list of AuthenticationProviders to be defined. Also allows you to define an alias to allow you to reference the AuthenticationManager in your own beans. - element authentication-manager {authman.attlist & authentication-provider* & ldap-authentication-provider*} -authman.attlist &= - id? -authman.attlist &= - ## An alias you wish to use for the AuthenticationManager bean (not required it you are using a specific id) - attribute alias {xsd:token}? -authman.attlist &= - ## If set to true, the AuthenticationManger will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. - attribute erase-credentials {xsd:boolean}? -authman.attlist &= - ## Use this ObservationRegistry to collect metrics on various parts of the filter chain - attribute observation-registry-ref {xsd:token}? - -authentication-provider = - ## Indicates that the contained user-service should be used as an authentication source. - element authentication-provider {ap.attlist & any-user-service & password-encoder?} -ap.attlist &= - ## Specifies a reference to a separately configured AuthenticationProvider instance which should be registered within the AuthenticationManager. - ref? -ap.attlist &= - ## Specifies a reference to a separately configured UserDetailsService from which to obtain authentication data. - user-service-ref? - -user-service = - ## Creates an in-memory UserDetailsService from a properties file or a list of "user" child elements. Usernames are converted to lower-case internally to allow for case-insensitive lookups, so this should not be used if case-sensitivity is required. - element user-service {id? & (properties-file | (user*))} -properties-file = - ## The location of a Properties file where each line is in the format of username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] - attribute properties {xsd:token}? - -user = - ## Represents a user in the application. - element user {user.attlist, empty} -user.attlist &= - ## The username assigned to the user. - attribute name {xsd:token} -user.attlist &= - ## The password assigned to the user. This may be hashed if the corresponding authentication provider supports hashing (remember to set the "hash" attribute of the "user-service" element). This attribute be omitted in the case where the data will not be used for authentication, but only for accessing authorities. If omitted, the namespace will generate a random value, preventing its accidental use for authentication. Cannot be empty. - attribute password {xsd:string}? -user.attlist &= - ## One of more authorities granted to the user. Separate authorities with a comma (but no space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" - attribute authorities {xsd:token} -user.attlist &= - ## Can be set to "true" to mark an account as locked and unusable. - attribute locked {xsd:boolean}? -user.attlist &= - ## Can be set to "true" to mark an account as disabled and unusable. - attribute disabled {xsd:boolean}? - -jdbc-user-service = - ## Causes creation of a JDBC-based UserDetailsService. - element jdbc-user-service {id? & jdbc-user-service.attlist} -jdbc-user-service.attlist &= - ## The bean ID of the DataSource which provides the required tables. - attribute data-source-ref {xsd:token} -jdbc-user-service.attlist &= - cache-ref? -jdbc-user-service.attlist &= - ## An SQL statement to query a username, password, and enabled status given a username. Default is "select username,password,enabled from users where username = ?" - attribute users-by-username-query {xsd:token}? -jdbc-user-service.attlist &= - ## An SQL statement to query for a user's granted authorities given a username. The default is "select username, authority from authorities where username = ?" - attribute authorities-by-username-query {xsd:token}? -jdbc-user-service.attlist &= - ## An SQL statement to query user's group authorities given a username. The default is "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" - attribute group-authorities-by-username-query {xsd:token}? -jdbc-user-service.attlist &= - role-prefix? - -csrf = -## Element for configuration of the CsrfFilter for protection against CSRF. It also updates the default RequestCache to only replay "GET" requests. - element csrf {csrf-options.attlist} -csrf-options.attlist &= - ## Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is enabled). - attribute disabled {xsd:boolean}? -csrf-options.attlist &= - ## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" - attribute request-matcher-ref { xsd:token }? -csrf-options.attlist &= - ## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository. - attribute token-repository-ref { xsd:token }? -csrf-options.attlist &= - ## The CsrfTokenRequestHandler to use. The default is CsrfTokenRequestAttributeHandler. - attribute request-handler-ref { xsd:token }? - -headers = -## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. -element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & permissions-policy? & cross-origin-opener-policy? & cross-origin-embedder-policy? & cross-origin-resource-policy? & header*)} -headers-options.attlist &= - ## Specifies if the default headers should be disabled. Default false. - attribute defaults-disabled {xsd:token}? -headers-options.attlist &= - ## Specifies if headers should be disabled. Default false. - attribute disabled {xsd:token}? -hsts = - ## Adds support for HTTP Strict Transport Security (HSTS) - element hsts {hsts-options.attlist} -hsts-options.attlist &= - ## Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. - attribute disabled {xsd:boolean}? -hsts-options.attlist &= - ## Specifies if subdomains should be included. Default true. - attribute include-subdomains {xsd:boolean}? -hsts-options.attlist &= - ## Specifies the maximum amount of time the host should be considered a Known HSTS Host. Default one year. - attribute max-age-seconds {xsd:integer}? -hsts-options.attlist &= - ## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true. - attribute request-matcher-ref { xsd:token }? -hsts-options.attlist &= - ## Specifies if preload should be included. Default false. - attribute preload {xsd:boolean}? - -cors = -## Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is specified a HandlerMappingIntrospector is used as the CorsConfigurationSource -element cors { cors-options.attlist } -cors-options.attlist &= - ref? -cors-options.attlist &= - ## Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to use - attribute configuration-source-ref {xsd:token}? - -hpkp = - ## Adds support for HTTP Public Key Pinning (HPKP). - element hpkp {hpkp.pins,hpkp.attlist} -hpkp.pins = - ## The list with pins - element pins {hpkp.pin+} -hpkp.pin = - ## A pin is specified using the base64-encoded SPKI fingerprint as value and the cryptographic hash algorithm as attribute - element pin { - ## The cryptographic hash algorithm - attribute algorithm { xsd:string }?, - text - } -hpkp.attlist &= - ## Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. - attribute disabled {xsd:boolean}? -hpkp.attlist &= - ## Specifies if subdomains should be included. Default false. - attribute include-subdomains {xsd:boolean}? -hpkp.attlist &= - ## Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. - attribute max-age-seconds {xsd:integer}? -hpkp.attlist &= - ## Specifies if the browser should only report pin validation failures. Default true. - attribute report-only {xsd:boolean}? -hpkp.attlist &= - ## Specifies the URI to which the browser should report pin validation failures. - attribute report-uri {xsd:string}? - -content-security-policy = - ## Adds support for Content Security Policy (CSP) - element content-security-policy {csp-options.attlist} -csp-options.attlist &= - ## The security policy directive(s) for the Content-Security-Policy header or if report-only is set to true, then the Content-Security-Policy-Report-Only header is used. - attribute policy-directives {xsd:token}? -csp-options.attlist &= - ## Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy violations only. Defaults to false. - attribute report-only {xsd:boolean}? - -referrer-policy = - ## Adds support for Referrer Policy - element referrer-policy {referrer-options.attlist} -referrer-options.attlist &= - ## The policies for the Referrer-Policy header. - attribute policy {"no-referrer","no-referrer-when-downgrade","same-origin","origin","strict-origin","origin-when-cross-origin","strict-origin-when-cross-origin","unsafe-url"}? - -feature-policy = - ## Adds support for Feature Policy - element feature-policy {feature-options.attlist} -feature-options.attlist &= - ## The security policy directive(s) for the Feature-Policy header. - attribute policy-directives {xsd:token}? - -permissions-policy = - ## Adds support for Permissions Policy - element permissions-policy {permissions-options.attlist} -permissions-options.attlist &= - ## The policies for the Permissions-Policy header. - attribute policy {xsd:token}? - -cache-control = - ## Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for every request - element cache-control {cache-control.attlist} -cache-control.attlist &= - ## Specifies if Cache Control should be disabled. Default false. - attribute disabled {xsd:boolean}? - -frame-options = - ## Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options header. - element frame-options {frame-options.attlist,empty} -frame-options.attlist &= - ## If disabled, the X-Frame-Options header will not be included. Default false. - attribute disabled {xsd:boolean}? -frame-options.attlist &= - ## Specify the policy to use for the X-Frame-Options-Header. - attribute policy {"DENY","SAMEORIGIN","ALLOW-FROM"}? -frame-options.attlist &= - ## Specify the strategy to use when ALLOW-FROM is chosen. - attribute strategy {"static","whitelist","regexp"}? -frame-options.attlist &= - ## Specify a reference to the custom AllowFromStrategy to use when ALLOW-FROM is chosen. - ref? -frame-options.attlist &= - ## Specify a value to use for the chosen strategy. - attribute value {xsd:string}? -frame-options.attlist &= - ## Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' based strategy. Default is 'from'. - ## Deprecated ALLOW-FROM is an obsolete directive that no longer works in modern browsers. Instead use - ## Content-Security-Policy with the - ## frame-ancestors - ## directive. - attribute from-parameter {xsd:string}? - - -xss-protection = - ## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header. - element xss-protection {xss-protection.attlist,empty} -xss-protection.attlist &= - ## disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. - attribute disabled {xsd:boolean}? -xss-protection.attlist &= - ## Specify the value for the X-Xss-Protection header. Defaults to "0". - attribute header-value {"0"|"1"|"1; mode=block"}? - -content-type-options = - ## Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. - element content-type-options {content-type-options.attlist, empty} -content-type-options.attlist &= - ## If disabled, the X-Content-Type-Options header will not be included. Default false. - attribute disabled {xsd:boolean}? - -cross-origin-opener-policy = - ## Adds support for Cross-Origin-Opener-Policy header - element cross-origin-opener-policy {cross-origin-opener-policy-options.attlist,empty} -cross-origin-opener-policy-options.attlist &= - ## The policies for the Cross-Origin-Opener-Policy header. - attribute policy {"unsafe-none","same-origin","same-origin-allow-popups"}? - -cross-origin-embedder-policy = - ## Adds support for Cross-Origin-Embedder-Policy header - element cross-origin-embedder-policy {cross-origin-embedder-policy-options.attlist,empty} -cross-origin-embedder-policy-options.attlist &= - ## The policies for the Cross-Origin-Embedder-Policy header. - attribute policy {"unsafe-none","require-corp"}? - -cross-origin-resource-policy = - ## Adds support for Cross-Origin-Resource-Policy header - element cross-origin-resource-policy {cross-origin-resource-policy-options.attlist,empty} -cross-origin-resource-policy-options.attlist &= - ## The policies for the Cross-Origin-Resource-Policy header. - attribute policy {"cross-origin","same-origin","same-site"}? - -header= - ## Add additional headers to the response. - element header {header.attlist} -header.attlist &= - ## The name of the header to add. - attribute name {xsd:token}? -header.attlist &= - ## The value for the header. - attribute value {xsd:token}? -header.attlist &= - ## Reference to a custom HeaderWriter implementation. - ref? - -any-user-service = user-service | jdbc-user-service | ldap-user-service - -custom-filter = - ## Used to indicate that a filter bean declaration should be incorporated into the security filter chain. - element custom-filter {custom-filter.attlist} - -custom-filter.attlist &= - ref - -custom-filter.attlist &= - (after | before | position) - -after = - ## The filter immediately after which the custom-filter should be placed in the chain. This feature will only be needed by advanced users who wish to mix their own filters into the security filter chain and have some knowledge of the standard Spring Security filters. The filter names map to specific Spring Security implementation filters. - attribute after {named-security-filter} -before = - ## The filter immediately before which the custom-filter should be placed in the chain - attribute before {named-security-filter} -position = - ## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter. - attribute position {named-security-filter} - -named-security-filter = "FIRST" | "DISABLE_ENCODE_URL_FILTER" | "FORCE_EAGER_SESSION_FILTER" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "SAML2_LOGOUT_REQUEST_FILTER" | "SAML2_LOGOUT_RESPONSE_FILTER" | "CSRF_FILTER" | "SAML2_LOGOUT_FILTER" | "LOGOUT_FILTER" | "OAUTH2_AUTHORIZATION_REQUEST_FILTER" | "SAML2_AUTHENTICATION_REQUEST_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "OAUTH2_LOGIN_FILTER" | "SAML2_AUTHENTICATION_FILTER" | "FORM_LOGIN_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER" | "WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.2.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-6.2.xsd deleted file mode 100644 index f123ad830a..0000000000 --- a/config/src/main/resources/org/springframework/security/config/spring-security-6.2.xsd +++ /dev/null @@ -1,3812 +0,0 @@ - - - - - - Defines the hashing algorithm used on user passwords. Bcrypt is recommended. - - - - - - - - - - - - - Whether a string should be base64 encoded - - - - - - - - Defines the strategy use for matching incoming requests. Currently the options are 'mvc' - (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions - and 'ciRegex' for case-insensitive regular expressions. - - - - - - - - - - - - - - - - Specifies an IP port number. Used to configure an embedded LDAP server, for example. - - - - - - - - Specifies a URL. - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - - - Defines a reference to a Spring bean Id. - - - - - - - - Defines a reference to a cache for use with a UserDetailsService. - - - - - - - - A reference to a user-service (or UserDetailsService bean) Id - - - - - - - - A reference to an AuthenticationManager bean - - - - - - - - A reference to a DataSource bean - - - - - - - Enables Spring Security debugging infrastructure. This will provide human-readable - (multi-line) debugging information to monitor requests coming into the security filters. - This may include sensitive information, such as request parameters or headers, and should - only be used in a development environment. - - - - - - - - - Defines a reference to a Spring bean Id. - - - - - - Defines the hashing algorithm used on user passwords. Bcrypt is recommended. - - - - - - - - - - - - - A non-empty string prefix that will be added to role strings loaded from persistent - storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is - non-empty. - - - - - - - - Enables the use of expressions in the 'access' attributes in <intercept-url> elements - rather than the traditional list of configuration attributes. Defaults to 'true'. If - enabled, each attribute should contain a single boolean expression. If the expression - evaluates to 'true', access will be granted. - - - - - - - Defines an LDAP server location or starts an embedded server. The url indicates the - location of a remote server. If no url is given, an embedded server will be started, - listening on the supplied port number. The port is optional and defaults to 33389. A - Spring LDAP ContextSource bean will be registered for the server with the id supplied. - - - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - Specifies a URL. - - - - - - Specifies an IP port number. Used to configure an embedded LDAP server, for example. - - - - - - Username (DN) of the "manager" user identity which will be used to authenticate to a - (non-embedded) LDAP server. If omitted, anonymous access will be used. - - - - - - The password for the manager DN. This is required if the manager-dn is specified. - - - - - - Explicitly specifies an ldif file resource to load into an embedded LDAP server. The - default is classpath*:*.ldiff - - - - - - Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" - - - - - - Explicitly specifies which embedded ldap server should use. Values are 'apacheds' and - 'unboundid'. By default, it will depends if the library is available in the classpath. - - - - - - - - - - - - - - The optional server to use. If omitted, and a default LDAP server is registered (using - <ldap-server> with no Id), that server will be used. - - - - - - - - Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN - of the user. - - - - - - - - Search base for group membership searches. Defaults to "" (searching from the root). - - - - - - - - The LDAP filter used to search for users (optional). For example "(uid={0})". The - substituted parameter is the user's login name. - - - - - - - - Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. - - - - - - - - The LDAP attribute name which contains the role name which will be used within Spring - Security. Defaults to "cn". - - - - - - - - Allows the objectClass of the user entry to be specified. If set, the framework will - attempt to load standard attributes for the defined class into the returned UserDetails - object - - - - - - - - - - - - - - Allows explicit customization of the loaded user object by specifying a - UserDetailsContextMapper bean which will be called with the context information from the - user's directory entry - - - - - - - This element configures a LdapUserDetailsService which is a combination of a - FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. - - - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - The optional server to use. If omitted, and a default LDAP server is registered (using - <ldap-server> with no Id), that server will be used. - - - - - - The LDAP filter used to search for users (optional). For example "(uid={0})". The - substituted parameter is the user's login name. - - - - - - Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. - - - - - - Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN - of the user. - - - - - - Search base for group membership searches. Defaults to "" (searching from the root). - - - - - - The LDAP attribute name which contains the role name which will be used within Spring - Security. Defaults to "cn". - - - - - - Defines a reference to a cache for use with a UserDetailsService. - - - - - - A non-empty string prefix that will be added to role strings loaded from persistent - storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is - non-empty. - - - - - - Allows the objectClass of the user entry to be specified. If set, the framework will - attempt to load standard attributes for the defined class into the returned UserDetails - object - - - - - - - - - - - - Allows explicit customization of the loaded user object by specifying a - UserDetailsContextMapper bean which will be called with the context information from the - user's directory entry - - - - - - - - - The optional server to use. If omitted, and a default LDAP server is registered (using - <ldap-server> with no Id), that server will be used. - - - - - - Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. - - - - - - The LDAP filter used to search for users (optional). For example "(uid={0})". The - substituted parameter is the user's login name. - - - - - - Search base for group membership searches. Defaults to "" (searching from the root). - - - - - - Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN - of the user. - - - - - - The LDAP attribute name which contains the role name which will be used within Spring - Security. Defaults to "cn". - - - - - - A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key - "{0}" must be present and will be substituted with the username. - - - - - - A non-empty string prefix that will be added to role strings loaded from persistent - storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is - non-empty. - - - - - - Allows the objectClass of the user entry to be specified. If set, the framework will - attempt to load standard attributes for the defined class into the returned UserDetails - object - - - - - - - - - - - - Allows explicit customization of the loaded user object by specifying a - UserDetailsContextMapper bean which will be called with the context information from the - user's directory entry - - - - - - - - - The attribute in the directory which contains the user password. Defaults to - "userPassword". - - - - - - Defines the hashing algorithm used on user passwords. Bcrypt is recommended. - - - - - - - - - - - - Can be used inside a bean definition to add a security interceptor to the bean and set up - access configuration attributes for the bean's methods - - - - - - - Defines a protected method and the access control configuration attributes that apply to - it. We strongly advise you NOT to mix "protect" declarations with any services provided - "global-method-security". - - - - - - - - - - - - - - Optional AccessDecisionManager bean ID to be used by the created method security - interceptor. - - - - - - Use the AuthorizationManager API instead of AccessDecisionManager (defaults to true) - - - - - - Use this AuthorizationManager instead of the default (supercedes - use-authorization-manager) - - - - - - - - - A method name - - - - - - Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". - - - - - - - Creates a MethodSecurityMetadataSource instance - - - - - - - Defines a protected method and the access control configuration attributes that apply to - it. We strongly advise you NOT to mix "protect" declarations with any services provided - "global-method-security". - - - - - - - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - Enables the use of expressions in the 'access' attributes in <intercept-url> elements - rather than the traditional list of configuration attributes. Defaults to 'true'. If - enabled, each attribute should contain a single boolean expression. If the expression - evaluates to 'true', access will be granted. - - - - - - - Provides method security for all beans registered in the Spring application context. - Specifically, beans will be scanned for matches with Spring Security annotations. Where - there is a match, the beans will automatically be proxied and security authorization - applied to the methods accordingly. Interceptors are invoked in the order specified in - AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. - Also, annotation-based interception can be overridden by expressions listed in - <protect-pointcut> elements. - - - - - - - Defines the SecurityExpressionHandler instance which will be used if expression-based - access-control is enabled. A default implementation (with no ACL support) will be used if - not supplied. - - - - - - - - - Defines a protected pointcut and the access control configuration attributes that apply to - it. Every bean registered in the Spring application context that provides a method that - matches the pointcut will receive security authorization. - - - - - - - - - - - - - - Specifies whether the use of Spring Security's pre and post invocation annotations - (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this - application context. Defaults to "true". - - - - - - Specifies whether the use of Spring Security's @Secured annotations should be enabled for - this application context. Defaults to "false". - - - - - - Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). - This will require the javax.annotation.security classes on the classpath. Defaults to - "false". - - - - - - If true, class-based proxying will be used instead of interface-based proxying. - - - - - - If set to aspectj, then use AspectJ to intercept method invocation - - - - - - - - - - - Specifies the security context holder strategy to use, by default uses a ThreadLocal-based - strategy - - - - - - Use this ObservationRegistry to collect metrics on various parts of the filter chain - - - - - - - Provides method security for all beans registered in the Spring application context. - Specifically, beans will be scanned for matches with the ordered list of - "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a - match, the beans will automatically be proxied and security authorization applied to the - methods accordingly. If you use and enable all four sources of method security metadata - (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 - security annotations), the metadata sources will be queried in that order. In practical - terms, this enables you to use XML to override method security metadata expressed in - annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize - etc.), @Secured and finally JSR-250. - - - - - - - - Allows the default expression-based mechanism for handling Spring Security's pre and post - invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be - replace entirely. Only applies if these annotations are enabled. - - - - - - - Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and - post invocation metadata from the annotated methods. - - - - - - - - - Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the - PreInvocationAuthorizationAdviceVoter for the <pre-post-annotation-handling> element. - - - - - - - - - Customizes the PostInvocationAdviceProvider with the ref as the - PostInvocationAuthorizationAdvice for the <pre-post-annotation-handling> element. - - - - - - - - - - - - Defines the SecurityExpressionHandler instance which will be used if expression-based - access-control is enabled. A default implementation (with no ACL support) will be used if - not supplied. - - - - - - - - - - Defines a protected pointcut and the access control configuration attributes that apply to - it. Every bean registered in the Spring application context that provides a method that - matches the pointcut will receive security authorization. - - - - - - - - - Allows addition of extra AfterInvocationProvider beans which should be called by the - MethodSecurityInterceptor created by global-method-security. - - - - - - - - - - - - - - Specifies whether the use of Spring Security's pre and post invocation annotations - (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this - application context. Defaults to "disabled". - - - - - - - - - - - - Specifies whether the use of Spring Security's @Secured annotations should be enabled for - this application context. Defaults to "disabled". - - - - - - - - - - - - Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). - This will require the javax.annotation.security classes on the classpath. Defaults to - "disabled". - - - - - - - - - - - - Optional AccessDecisionManager bean ID to override the default used for method security. - - - - - - Optional RunAsmanager implementation which will be used by the configured - MethodSecurityInterceptor - - - - - - Allows the advice "order" to be set for the method security interceptor. - - - - - - If true, class based proxying will be used instead of interface based proxying. - - - - - - Can be used to specify that AspectJ should be used instead of the default Spring AOP. If - set, secured classes must be woven with the AnnotationSecurityAspect from the - spring-security-aspects module. - - - - - - - - - - - An external MethodSecurityMetadataSource instance can be supplied which will take priority - over other sources (such as the default annotations). - - - - - - A reference to an AuthenticationManager bean - - - - - - - - - - - - - - - An AspectJ expression, including the 'execution' keyword. For example, 'execution(int - com.foo.TargetObject.countLength(String))' (without the quotes). - - - - - - Access configuration attributes list that applies to all methods matching the pointcut, - e.g. "ROLE_A,ROLE_B" - - - - - - - Allows securing a Message Broker. There are two modes. If no id is specified: ensures that - any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver - registered as a custom argument resolver; ensures that the - SecurityContextChannelInterceptor is automatically registered for the - clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the - clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that - can be manually registered with the clientInboundChannel. - - - - - - - - Defines the SecurityExpressionHandler instance which will be used if expression-based - access-control is enabled. A default implementation (with no ACL support) will be used if - not supplied. - - - - - - - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. If specified, - explicit configuration within clientInboundChannel is required. If not specified, ensures - that any SimpAnnotationMethodMessageHandler has the - AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures - that the SecurityContextChannelInterceptor is automatically registered for the - clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the - clientInboundChannel. - - - - - - Disables the requirement for CSRF token to be present in the Stomp headers (default - false). Changing the default is useful if it is necessary to allow other origins to make - SockJS connections. - - - - - - Use this AuthorizationManager instead of deriving one from <intercept-message> elements - - - - - - Use AuthorizationManager API instead of SecurityMetadatasource (defaults to true) - - - - - - Use this SecurityContextHolderStrategy (note only supported in conjunction with the - AuthorizationManager API) - - - - - - - Creates an authorization rule for a websocket message. - - - - - - - - - - The destination ant pattern which will be mapped to the access attribute. For example, /** - matches any message with a destination, /admin/** matches any message that has a - destination that starts with admin. - - - - - - The access configuration attributes that apply for the configured message. For example, - permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role - 'ROLE_ADMIN'. - - - - - - The type of message to match on. Valid values are defined in SimpMessageType (i.e. - CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, - DISCONNECT_ACK, OTHER). - - - - - - - - - - - - - - - - - - - - Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created - by the namespace. - - - - - - - - - Container element for HTTP security configuration. Multiple elements can now be defined, - each with a specific pattern to which the enclosed security configuration applies. A - pattern can also be configured to bypass Spring Security's filters completely by setting - the "security" attribute to "none". - - - - - - - Specifies the access attributes and/or filter list for a particular set of URLs. - - - - - - - - - Defines the access-denied strategy that should be used. An access denied page can be - defined or a reference to an AccessDeniedHandler instance. - - - - - - - - - Sets up a form login configuration for authentication with a username and password - - - - - - - - - - - - Configures authentication support for SAML 2.0 Login - - - - - - - - - Configures SAML 2.0 Single Logout support - - - - - - - - - Adds support for X.509 client authentication. - - - - - - - - - - Adds support for basic authentication - - - - - - - - - Incorporates a logout processing filter. Most web applications require a logout filter, - although you may not require one if you write a controller to provider similar logic. - - - - - - - - - - Session-management related functionality is implemented by the addition of a - SessionManagementFilter to the filter stack. - - - - - - - Enables concurrent session control, limiting the number of authenticated sessions a user - may have at the same time. - - - - - - - - - - - - - Sets up remember-me authentication. If used with the "key" attribute (or no attributes) - the cookie-only implementation will be used. Specifying "token-repository-ref" or - "remember-me-data-source-ref" will use the more secure, persisten token approach. - - - - - - - - - Adds support for automatically granting all anonymous web requests a particular principal - identity and a corresponding granted authority. - - - - - - - - - Defines the list of mappings between http and https ports for use in redirects - - - - - - - Provides a method to map http ports to https ports when forcing a redirect. - - - - - - - - - - - - - - - Defines the SecurityExpressionHandler instance which will be used if expression-based - access-control is enabled. A default implementation (with no ACL support) will be used if - not supplied. - - - - - - - - - - - - - - - - - The request URL pattern which will be mapped to the filter chain created by this <http> - element. If omitted, the filter chain will match all requests. - - - - - - When set to 'none', requests matching the pattern attribute will be ignored by Spring - Security. No security filters will be applied and no SecurityContext will be available. If - set, the <http> element must be empty, with no children. - - - - - - - - - - - Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. - - - - - - A legacy attribute which automatically registers a login form, BASIC authentication and a - logout URL and logout services. If unspecified, defaults to "false". We'd recommend you - avoid using this and instead explicitly configure the services you require. - - - - - - Enables the use of expressions in the 'access' attributes in <intercept-url> elements - rather than the traditional list of configuration attributes. Defaults to 'true'. If - enabled, each attribute should contain a single boolean expression. If the expression - evaluates to 'true', access will be granted. - - - - - - A reference to a SecurityContextHolderStrategy bean. This can be used to customize how the - SecurityContextHolder is stored during a request - - - - - - Controls the eagerness with which an HTTP session is created by Spring Security classes. - If not set, defaults to "ifRequired". If "stateless" is used, this implies that the - application guarantees that it will not create a session. This differs from the use of - "never" which means that Spring Security will not create a session, but will make use of - one if the application does. - - - - - - - - - - - - - - A reference to a SecurityContextRepository bean. This can be used to customize how the - SecurityContext is stored between requests. - - - - - - Optional attribute that specifies that the SecurityContext should require explicit saving - rather than being synchronized from the SecurityContextHolder. Defaults to "true". - - - - - - Defines the strategy use for matching incoming requests. Currently the options are 'mvc' - (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions - and 'ciRegex' for case-insensitive regular expressions. - - - - - - - - - - - - - - Provides versions of HttpServletRequest security methods such as isUserInRole() and - getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to - "true". - - - - - - If available, runs the request as the Subject acquired from the JaasAuthenticationToken. - Defaults to "false". - - - - - - Use AuthorizationManager API instead of SecurityMetadataSource (defaults to true) - - - - - - Use this AuthorizationManager instead of deriving one from <intercept-url> elements - - - - - - Optional attribute specifying the ID of the AccessDecisionManager implementation which - should be used for authorizing HTTP requests. - - - - - - Optional attribute specifying the realm name that will be used for all authentication - features that require a realm name (eg BASIC and Digest authentication). If unspecified, - defaults to "Spring Security Application". - - - - - - Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. - - - - - - Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults - to "false" - - - - - - Corresponds to the shouldFilterAllDispatcherTypes property of AuthorizationFilter. Do not - work when use-authorization-manager=false. Defaults to "true". - - - - - - Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" - (rewriting is disabled). - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - A reference to an AuthenticationManager bean - - - - - - Use this ObservationRegistry to collect metrics on various parts of the filter chain - - - - - - - - - Defines a reference to a Spring bean Id. - - - - - - The access denied page that an authenticated user will be redirected to if they request a - page which they don't have the authority to access. - - - - - - - - The access denied page that an authenticated user will be redirected to if they request a - page which they don't have the authority to access. - - - - - - - - - The request URL pattern which will be mapped to the FilterChain. - - - - - - Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. - - - - - - The access configuration attributes that apply for the configured path. - - - - - - The HTTP Method for which the access configuration attributes should apply. If not - specified, the attributes will apply to any method. - - - - - - - - - - - - - - - - - - Used to specify that a URL must be accessed over http or https, or that there is no - preference. The value should be "http", "https" or "any", respectively. - - - - - - The path to the servlet. This attribute is only applicable when 'request-matcher' is - 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are - 2 or more HttpServlet's registered in the ServletContext that have mappings starting with - '/' and are different; 2) The pattern starts with the same value of a registered - HttpServlet path, excluding the default (root) HttpServlet '/'. - - - - - - - - - Specifies the URL that will cause a logout. Spring Security will initialize a filter that - responds to this particular URL. Defaults to /logout if unspecified. - - - - - - Specifies the URL to display once the user has logged out. If not specified, defaults to - <form-login-login-page>/?logout (i.e. /login?logout). - - - - - - Specifies whether a logout also causes HttpSession invalidation, which is generally - desirable. If unspecified, defaults to true. - - - - - - A reference to a LogoutSuccessHandler implementation which will be used to determine the - destination to which the user is taken after logging out. - - - - - - A comma-separated list of the names of cookies which should be deleted when the user logs - out - - - - - - - Allow the RequestCache used for saving requests during the login process to be set - - - - - - - - - - - The URL that the login form is posted to. If unspecified, it defaults to /login. - - - - - - The name of the request parameter which contains the username. Defaults to 'username'. - - - - - - The name of the request parameter which contains the password. Defaults to 'password'. - - - - - - The URL that will be redirected to after successful authentication, if the user's previous - action could not be resumed. This generally happens if the user visits a login page - without having first requested a secured operation that triggers authentication. If - unspecified, defaults to the root of the application. - - - - - - Whether the user should always be redirected to the default-target-url after login. - - - - - - The URL for the login page. If no login URL is specified, Spring Security will - automatically create a login URL at GET /login and a corresponding filter to render that - login URL when requested. - - - - - - The URL for the login failure page. If no login failure URL is specified, Spring Security - will automatically create a failure login URL at /login?error and a corresponding filter - to render that login failure URL when requested. - - - - - - Reference to an AuthenticationSuccessHandler bean which should be used to handle a - successful authentication request. Should not be used in combination with - default-target-url (or always-use-default-target-url) as the implementation should always - deal with navigation to the subsequent destination - - - - - - Reference to an AuthenticationFailureHandler bean which should be used to handle a failed - authentication request. Should not be used in combination with authentication-failure-url - as the implementation should always deal with navigation to the subsequent destination - - - - - - Reference to an AuthenticationDetailsSource which will be used by the authentication - filter - - - - - - The URL for the ForwardAuthenticationFailureHandler - - - - - - The URL for the ForwardAuthenticationSuccessHandler - - - - - - - Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. - - - - - - - - - - Reference to the ClientRegistrationRepository - - - - - - Reference to the OAuth2AuthorizedClientRepository - - - - - - Reference to the OAuth2AuthorizedClientService - - - - - - Reference to the AuthorizationRequestRepository - - - - - - Reference to the OAuth2AuthorizationRequestResolver - - - - - - Reference to the authorization RedirectStrategy - - - - - - Reference to the OAuth2AccessTokenResponseClient - - - - - - Reference to the GrantedAuthoritiesMapper - - - - - - Reference to the OAuth2UserService - - - - - - Reference to the OpenID Connect OAuth2UserService - - - - - - The URI where the filter processes authentication requests - - - - - - The URI to send users to login - - - - - - Reference to the AuthenticationSuccessHandler - - - - - - Reference to the AuthenticationFailureHandler - - - - - - Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider - - - - - - - Configures OAuth 2.0 Client support. - - - - - - - - - - - - - Reference to the ClientRegistrationRepository - - - - - - Reference to the OAuth2AuthorizedClientRepository - - - - - - Reference to the OAuth2AuthorizedClientService - - - - - - - Configures OAuth 2.0 Authorization Code Grant. - - - - - - - - - - Reference to the AuthorizationRequestRepository - - - - - - Reference to the authorization RedirectStrategy - - - - - - Reference to the OAuth2AuthorizationRequestResolver - - - - - - Reference to the OAuth2AccessTokenResponseClient - - - - - - - Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 - Provider. - - - - - - - - - - - - Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. - - - - - - - - - - The ID that uniquely identifies the client registration. - - - - - - The client identifier. - - - - - - The client secret. - - - - - - The method used to authenticate the client with the provider. The supported values are - client_secret_basic, client_secret_post and none (public clients). - - - - - - - - - - - - - - - The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The - supported values are authorization_code, client_credentials and password. - - - - - - - - - - - - - The client’s registered redirect URI that the Authorization Server redirects the - end-user’s user-agent to after the end-user has authenticated and authorized access to the - client. - - - - - - A comma-separated list of scope(s) requested by the client during the Authorization - Request flow, such as openid, email, or profile. - - - - - - A descriptive name used for the client. The name may be used in certain scenarios, such as - when displaying the name of the client in the auto-generated login page. - - - - - - A reference to the associated provider. May reference a 'provider' element or use one of - the common providers (google, github, facebook, okta). - - - - - - - The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. - - - - - - - - - - The ID that uniquely identifies the provider. - - - - - - The Authorization Endpoint URI for the Authorization Server. - - - - - - The Token Endpoint URI for the Authorization Server. - - - - - - The UserInfo Endpoint URI used to access the claims/attributes of the authenticated - end-user. - - - - - - The authentication method used when sending the access token to the UserInfo Endpoint. The - supported values are header, form and query. - - - - - - - - - - - - - The name of the attribute returned in the UserInfo Response that references the Name or - Identifier of the end-user. - - - - - - The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server, which - contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID - Token and optionally the UserInfo Response. - - - - - - The URI used to discover the configuration information for an OAuth 2.0 or OpenID Connect - 1.0 Provider. - - - - - - - Configures authentication support as an OAuth 2.0 Resource Server. - - - - - - - - - - - - - - Reference to an AuthenticationManagerResolver - - - - - - Reference to a BearerTokenResolver - - - - - - Reference to a AuthenticationEntryPoint - - - - - - - Configures JWT authentication - - - - - - - - - - The URI to use to collect the JWK Set for verifying JWTs - - - - - - Reference to a JwtDecoder - - - - - - Reference to a Converter<Jwt, AbstractAuthenticationToken> - - - - - - - Configuration Opaque Token authentication - - - - - - - - - - The URI to use to introspect opaque token attributes - - - - - - The Client ID to use to authenticate the introspection request - - - - - - The Client secret to use to authenticate the introspection request - - - - - - Reference to an OpaqueTokenIntrospector - - - - - - Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful - introspection result into an Authentication. - - - - - - - - - Reference to the RelyingPartyRegistrationRepository - - - - - - Reference to the Saml2AuthenticationRequestRepository - - - - - - Reference to the Saml2AuthenticationRequestResolver - - - - - - Reference to the AuthenticationConverter - - - - - - The URI where the filter processes authentication requests - - - - - - The URI to send users to login - - - - - - Reference to the AuthenticationSuccessHandler - - - - - - Reference to the AuthenticationFailureHandler - - - - - - Reference to the AuthenticationManager - - - - - - - - - The URL by which the relying or asserting party can trigger logout - - - - - - The URL by which the asserting party can send a SAML 2.0 Logout Request - - - - - - The URL by which the asserting party can send a SAML 2.0 Logout Response - - - - - - Reference to the RelyingPartyRegistrationRepository - - - - - - Reference to the Saml2LogoutRequestValidator - - - - - - Reference to the Saml2LogoutRequestResolver - - - - - - Reference to the Saml2LogoutRequestRepository - - - - - - Reference to the Saml2LogoutResponseValidator - - - - - - Reference to the Saml2LogoutResponseResolver - - - - - - - Container element for relying party(ies) registered with a SAML 2.0 identity provider - - - - - - - - - - - - Represents a relying party registered with a SAML 2.0 identity provider - - - - - - - - - - - - - - The ID that uniquely identifies the relying party registration. - - - - - - The location of the Identity Provider's metadata. - - - - - - The relying party's EntityID - - - - - - The Assertion Consumer Service Location - - - - - - The Assertion Consumer Service Binding - - - - - - A reference to the associated asserting party. - - - - - - The relying party <a - href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService - Location</a> - - - - - - The relying party <a - href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService - Response Location</a> - - - - - - The relying party <a - href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService - Binding</a> - - - - - - - The relying party's signing credential - - - - - - - - - - The private key location - - - - - - The certificate location - - - - - - - The relying party's decryption credential - - - - - - - - - - The private key location - - - - - - The certificate location - - - - - - - The configuration metadata of the Asserting party - - - - - - - - - - - - - - A unique identifier of the asserting party. - - - - - - The asserting party's EntityID. - - - - - - Indicates the asserting party's preference that relying parties should sign the - AuthnRequest before sending - - - - - - The <a - href="https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint">SingleSignOnService</a> - Location. - - - - - - The <a - href="https://www.oasis-open.org/committees/download.php/51890/SAML%20MD%20simplified%20overview.pdf#2.5%20Endpoint">SingleSignOnService</a> - Binding. - - - - - - A comma separated list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this - asserting party, in preference order. - - - - - - The asserting party <a - href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService - Location</a> - - - - - - The asserting party <a - href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService - Response Location</a> - - - - - - The asserting party <a - href="https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf#page=7">SingleLogoutService - Binding</a> - - - - - - - The relying party's verification credential - - - - - - - - - - The private key location - - - - - - The certificate location - - - - - - - The asserting party's encryption credential - - - - - - - - - - The private key location - - - - - - The certificate location - - - - - - - Used to explicitly configure a FilterChainProxy instance with a FilterChainMap - - - - - - - - - - - - - Defines the strategy use for matching incoming requests. Currently the options are 'mvc' - (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions - and 'ciRegex' for case-insensitive regular expressions. - - - - - - - - - - - - - - - Used within to define a specific URL pattern and the list of filters which apply to the - URLs matching that pattern. When multiple filter-chain elements are assembled in a list in - order to configure a FilterChainProxy, the most specific patterns must be placed at the - top of the list, with most general ones at the bottom. - - - - - - - - - - The request URL pattern which will be mapped to the FilterChain. - - - - - - Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. - - - - - - A comma separated list of bean names that implement Filter that should be processed for - this FilterChain. If the value is none, then no Filters will be used for this FilterChain. - - - - - - - - The request URL pattern which will be mapped to the FilterChain. - - - - - - - - Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. - - - - - - - Used to explicitly configure a FilterSecurityMetadataSource bean for use with a - FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy - explicitly, rather than using the <http> element. The intercept-url elements used should - only contain pattern, method and access attributes. Any others will result in a - configuration error. - - - - - - - Specifies the access attributes and/or filter list for a particular set of URLs. - - - - - - - - - - - - - - Enables the use of expressions in the 'access' attributes in <intercept-url> elements - rather than the traditional list of configuration attributes. Defaults to 'true'. If - enabled, each attribute should contain a single boolean expression. If the expression - evaluates to 'true', access will be granted. - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - Defines the strategy use for matching incoming requests. Currently the options are 'mvc' - (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions - and 'ciRegex' for case-insensitive regular expressions. - - - - - - - - - - - - - - - - - Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. - - - - - - Reference to an AuthenticationDetailsSource which will be used by the authentication - filter - - - - - - - Adds support for the password management. - - - - - - - - - - The change password page. Defaults to "/change-password". - - - - - - - - - Specifies that SessionAuthenticationStrategy must be explicitly invoked. Default false - (i.e. SessionManagementFilter will implicitly invoke SessionAuthenticationStrategy). - - - - - - Indicates how session fixation protection will be applied when a user authenticates. If - set to "none", no protection will be applied. "newSession" will create a new empty - session, with only Spring Security-related attributes migrated. "migrateSession" will - create a new session and copy all session attributes to the new session. In Servlet 3.1 - (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing - session and use the container-supplied session fixation protection - (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and - newer containers, "migrateSession" in older containers. Throws an exception if - "changeSessionId" is used in older containers. - - - - - - - - - - - - - - The URL to which a user will be redirected if they submit an invalid session indentifier. - Typically used to detect session timeouts. - - - - - - Allows injection of the InvalidSessionStrategy instance used by the - SessionManagementFilter - - - - - - Allows injection of the SessionAuthenticationStrategy instance used by the - SessionManagementFilter - - - - - - Defines the URL of the error page which should be shown when the - SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error - code will be returned to the client. Note that this attribute doesn't apply if the error - occurs during a form-based login, where the URL for authentication failure will take - precedence. - - - - - - - - - The maximum number of sessions a single authenticated user can have open at the same time. - Defaults to "1". A negative value denotes unlimited sessions. - - - - - - The URL a user will be redirected to if they attempt to use a session which has been - "expired" because they have logged in again. - - - - - - Allows injection of the SessionInformationExpiredStrategy instance used by the - ConcurrentSessionFilter - - - - - - Specifies that an unauthorized error should be reported when a user attempts to login when - they already have the maximum configured sessions open. The default behaviour is to expire - the original session. If the session-authentication-error-url attribute is set on the - session-management URL, the user will be redirected to this URL. - - - - - - Allows you to define an alias for the SessionRegistry bean in order to access it in your - own configuration. - - - - - - Allows you to define an external SessionRegistry bean to be used by the concurrency - control setup. - - - - - - - - - The "key" used to identify cookies from a specific token-based remember-me application. - You should set this to a unique value for your application. If unset, it will default to a - random value generated by SecureRandom. - - - - - - Reference to a PersistentTokenRepository bean for use with the persistent token - remember-me implementation. - - - - - - A reference to a DataSource bean - - - - - - - A reference to a user-service (or UserDetailsService bean) Id - - - - - - Exports the internally defined RememberMeServices as a bean alias, allowing it to be used - by other beans in the application context. - - - - - - Determines whether the "secure" flag will be set on the remember-me cookie. If set to - true, the cookie will only be submitted over HTTPS (recommended). By default, secure - cookies will be used if the request is made on a secure connection. - - - - - - The period (in seconds) for which the remember-me cookie should be valid. - - - - - - Reference to an AuthenticationSuccessHandler bean which should be used to handle a - successful remember-me authentication. - - - - - - The name of the request parameter which toggles remember-me authentication. Defaults to - 'remember-me'. - - - - - - The name of cookie which store the token for remember-me authentication. Defaults to - 'remember-me'. - - - - - - - - Reference to a PersistentTokenRepository bean for use with the persistent token - remember-me implementation. - - - - - - - - Allows a custom implementation of RememberMeServices to be used. Note that this - implementation should return RememberMeAuthenticationToken instances with the same "key" - value as specified in the remember-me element. Alternatively it should register its own - AuthenticationProvider. It should also implement the LogoutHandler interface, which will - be invoked when a user logs out. Typically the remember-me cookie would be removed on - logout. - - - - - - - - - - - - The key shared between the provider and filter. This generally does not need to be set. If - unset, it will default to a random value generated by SecureRandom. - - - - - - The username that should be assigned to the anonymous request. This allows the principal - to be identified, which may be important for logging and auditing. if unset, defaults to - "anonymousUser". - - - - - - The granted authority that should be assigned to the anonymous request. Commonly this is - used to assign the anonymous request particular roles, which can subsequently be used in - authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". - - - - - - With the default namespace setup, the anonymous "authentication" facility is automatically - enabled. You can disable it using this property. - - - - - - - - - - The http port to use. - - - - - - - - The https port to use. - - - - - - - - - The regular expression used to obtain the username from the certificate's subject. - Defaults to matching on the common name using the pattern "CN=(.*?),". - - - - - - A reference to a user-service (or UserDetailsService bean) Id - - - - - - Reference to an AuthenticationDetailsSource which will be used by the authentication - filter - - - - - - - Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration - with container authentication. - - - - - - - - - - A comma-separate list of roles to look for in the incoming HttpServletRequest. - - - - - - A reference to a user-service (or UserDetailsService bean) Id - - - - - - - Registers the AuthenticationManager instance and allows its list of - AuthenticationProviders to be defined. Also allows you to define an alias to allow you to - reference the AuthenticationManager in your own beans. - - - - - - - Indicates that the contained user-service should be used as an authentication source. - - - - - - - - element which defines a password encoding strategy. Used by an authentication provider to - convert submitted passwords to hashed versions, for example. - - - - - - - - - - - - - Sets up an ldap authentication provider - - - - - - - Specifies that an LDAP provider should use an LDAP compare operation of the user's - password to authenticate the user - - - - - - - element which defines a password encoding strategy. Used by an authentication provider to - convert submitted passwords to hashed versions, for example. - - - - - - - - - - - - - - - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - An alias you wish to use for the AuthenticationManager bean (not required it you are using - a specific id) - - - - - - If set to true, the AuthenticationManger will attempt to clear any credentials data in the - returned Authentication object, once the user has been authenticated. - - - - - - Use this ObservationRegistry to collect metrics on various parts of the filter chain - - - - - - - - - Defines a reference to a Spring bean Id. - - - - - - A reference to a user-service (or UserDetailsService bean) Id - - - - - - - Creates an in-memory UserDetailsService from a properties file or a list of "user" child - elements. Usernames are converted to lower-case internally to allow for case-insensitive - lookups, so this should not be used if case-sensitivity is required. - - - - - - - Represents a user in the application. - - - - - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - - - - - The location of a Properties file where each line is in the format of - username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] - - - - - - - - - The username assigned to the user. - - - - - - The password assigned to the user. This may be hashed if the corresponding authentication - provider supports hashing (remember to set the "hash" attribute of the "user-service" - element). This attribute be omitted in the case where the data will not be used for - authentication, but only for accessing authorities. If omitted, the namespace will - generate a random value, preventing its accidental use for authentication. Cannot be - empty. - - - - - - One of more authorities granted to the user. Separate authorities with a comma (but no - space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" - - - - - - Can be set to "true" to mark an account as locked and unusable. - - - - - - Can be set to "true" to mark an account as disabled and unusable. - - - - - - - Causes creation of a JDBC-based UserDetailsService. - - - - - - A bean identifier, used for referring to the bean elsewhere in the context. - - - - - - - - - - The bean ID of the DataSource which provides the required tables. - - - - - - Defines a reference to a cache for use with a UserDetailsService. - - - - - - An SQL statement to query a username, password, and enabled status given a username. - Default is "select username,password,enabled from users where username = ?" - - - - - - An SQL statement to query for a user's granted authorities given a username. The default - is "select username, authority from authorities where username = ?" - - - - - - An SQL statement to query user's group authorities given a username. The default is - "select g.id, g.group_name, ga.authority from groups g, group_members gm, - group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" - - - - - - A non-empty string prefix that will be added to role strings loaded from persistent - storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is - non-empty. - - - - - - - Element for configuration of the CsrfFilter for protection against CSRF. It also updates - the default RequestCache to only replay "GET" requests. - - - - - - - - - - Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is - enabled). - - - - - - The RequestMatcher instance to be used to determine if CSRF should be applied. Default is - any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" - - - - - - The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by - LazyCsrfTokenRepository. - - - - - - The CsrfTokenRequestHandler to use. The default is CsrfTokenRequestAttributeHandler. - - - - - - - Element for configuration of the HeaderWritersFilter. Enables easy setting for the - X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. - - - - - - - - - - - - - - - - - - - - - - - - - - Specifies if the default headers should be disabled. Default false. - - - - - - Specifies if headers should be disabled. Default false. - - - - - - - Adds support for HTTP Strict Transport Security (HSTS) - - - - - - - - - - Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. - - - - - - Specifies if subdomains should be included. Default true. - - - - - - Specifies the maximum amount of time the host should be considered a Known HSTS Host. - Default one year. - - - - - - The RequestMatcher instance to be used to determine if the header should be set. Default - is if HttpServletRequest.isSecure() is true. - - - - - - Specifies if preload should be included. Default false. - - - - - - - Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is - specified a HandlerMappingIntrospector is used as the CorsConfigurationSource - - - - - - - - - - Defines a reference to a Spring bean Id. - - - - - - Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to - use - - - - - - - Adds support for HTTP Public Key Pinning (HPKP). - - - - - - - - - - - - - - - - - - The list with pins - - - - - - - - - - - A pin is specified using the base64-encoded SPKI fingerprint as value and the - cryptographic hash algorithm as attribute - - - - - - The cryptographic hash algorithm - - - - - - - - - Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. - - - - - - Specifies if subdomains should be included. Default false. - - - - - - Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. - - - - - - Specifies if the browser should only report pin validation failures. Default true. - - - - - - Specifies the URI to which the browser should report pin validation failures. - - - - - - - Adds support for Content Security Policy (CSP) - - - - - - - - - - The security policy directive(s) for the Content-Security-Policy header or if report-only - is set to true, then the Content-Security-Policy-Report-Only header is used. - - - - - - Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy - violations only. Defaults to false. - - - - - - - Adds support for Referrer Policy - - - - - - - - - - The policies for the Referrer-Policy header. - - - - - - - - - - - - - - - - - - - Adds support for Feature Policy - - - - - - - - - - The security policy directive(s) for the Feature-Policy header. - - - - - - - Adds support for Permissions Policy - - - - - - - - - - The policies for the Permissions-Policy header. - - - - - - - Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for - every request - - - - - - - - - - Specifies if Cache Control should be disabled. Default false. - - - - - - - Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options - header. - - - - - - - - - - If disabled, the X-Frame-Options header will not be included. Default false. - - - - - - Specify the policy to use for the X-Frame-Options-Header. - - - - - - - - - - - - - Specify the strategy to use when ALLOW-FROM is chosen. - - - - - - - - - - - - - Defines a reference to a Spring bean Id. - - - - - - Specify a value to use for the chosen strategy. - - - - - - Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' - based strategy. Default is 'from'. Deprecated ALLOW-FROM is an obsolete directive that no - longer works in modern browsers. Instead use Content-Security-Policy with the <a - href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors">frame-ancestors</a> - directive. - - - - - - - Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the - X-XSS-Protection header. - - - - - - - - - - disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. - - - - - - Specify the value for the X-Xss-Protection header. Defaults to "0". - - - - - - - - - - - - - - Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. - - - - - - - - - - If disabled, the X-Content-Type-Options header will not be included. Default false. - - - - - - - Adds support for Cross-Origin-Opener-Policy header - - - - - - - - - - The policies for the Cross-Origin-Opener-Policy header. - - - - - - - - - - - - - - Adds support for Cross-Origin-Embedder-Policy header - - - - - - - - - - The policies for the Cross-Origin-Embedder-Policy header. - - - - - - - - - - - - - Adds support for Cross-Origin-Resource-Policy header - - - - - - - - - - The policies for the Cross-Origin-Resource-Policy header. - - - - - - - - - - - - - - Add additional headers to the response. - - - - - - - - - - The name of the header to add. - - - - - - The value for the header. - - - - - - Defines a reference to a Spring bean Id. - - - - - - - - Used to indicate that a filter bean declaration should be incorporated into the security - filter chain. - - - - - - - - - - - The filter immediately after which the custom-filter should be placed in the chain. This - feature will only be needed by advanced users who wish to mix their own filters into the - security filter chain and have some knowledge of the standard Spring Security filters. The - filter names map to specific Spring Security implementation filters. - - - - - - The filter immediately before which the custom-filter should be placed in the chain - - - - - - The explicit position at which the custom-filter should be placed in the chain. Use if you - are replacing a standard filter. - - - - - - - - The filter immediately after which the custom-filter should be placed in the chain. This - feature will only be needed by advanced users who wish to mix their own filters into the - security filter chain and have some knowledge of the standard Spring Security filters. The - filter names map to specific Spring Security implementation filters. - - - - - - - - The filter immediately before which the custom-filter should be placed in the chain - - - - - - - - The explicit position at which the custom-filter should be placed in the chain. Use if you - are replacing a standard filter. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/config/src/test/java/org/springframework/security/config/MockServletContext.java b/config/src/test/java/org/springframework/security/config/MockServletContext.java index d819d4c798..67b7c396e7 100644 --- a/config/src/test/java/org/springframework/security/config/MockServletContext.java +++ b/config/src/test/java/org/springframework/security/config/MockServletContext.java @@ -55,11 +55,6 @@ public class MockServletContext extends org.springframework.mock.web.MockServlet return this.registrations; } - @Override - public ServletRegistration getServletRegistration(String servletName) { - return this.registrations.get(servletName); - } - private static class MockServletRegistration implements ServletRegistration.Dynamic { private final String name; diff --git a/config/src/test/java/org/springframework/security/config/TestMockHttpServletMappings.java b/config/src/test/java/org/springframework/security/config/TestMockHttpServletMappings.java deleted file mode 100644 index 3f1f7f797b..0000000000 --- a/config/src/test/java/org/springframework/security/config/TestMockHttpServletMappings.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.MappingMatch; - -import org.springframework.mock.web.MockHttpServletMapping; - -public final class TestMockHttpServletMappings { - - private TestMockHttpServletMappings() { - - } - - public static MockHttpServletMapping extension(HttpServletRequest request, String extension) { - String uri = request.getRequestURI(); - String matchValue = uri.substring(0, uri.lastIndexOf(extension)); - return new MockHttpServletMapping(matchValue, "*" + extension, "extension", MappingMatch.EXTENSION); - } - - public static MockHttpServletMapping path(HttpServletRequest request, String path) { - String uri = request.getRequestURI(); - String matchValue = uri.substring(path.length()); - return new MockHttpServletMapping(matchValue, path + "/*", "path", MappingMatch.PATH); - } - - public static MockHttpServletMapping defaultMapping() { - return new MockHttpServletMapping("", "/", "default", MappingMatch.DEFAULT); - } - -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurerTests.java index 06935c82cd..de5240661a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurerTests.java @@ -17,13 +17,11 @@ package org.springframework.security.config.annotation.authentication.configurers.provisioning; import java.util.Arrays; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @@ -75,7 +73,7 @@ public class UserDetailsManagerConfigurerTests { .authorities(authority) .build(); // @formatter:on - assertThat((Optional) userDetails.getAuthorities().stream().findFirst()).contains(authority); + assertThat(userDetails.getAuthorities().stream().findFirst().get()).isEqualTo(authority); } @Test @@ -101,7 +99,7 @@ public class UserDetailsManagerConfigurerTests { .authorities(Arrays.asList(authority)) .build(); // @formatter:on - assertThat((Optional) userDetails.getAuthorities().stream().findFirst()).contains(authority); + assertThat(userDetails.getAuthorities().stream().findFirst().get()).isEqualTo(authority); } private UserDetailsManagerConfigurer> configurer() { diff --git a/config/src/test/java/org/springframework/security/config/annotation/issue50/SecurityConfig.java b/config/src/test/java/org/springframework/security/config/annotation/issue50/SecurityConfig.java index ba7d4a616c..e3b1d8c42e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/issue50/SecurityConfig.java +++ b/config/src/test/java/org/springframework/security/config/annotation/issue50/SecurityConfig.java @@ -66,7 +66,7 @@ public class SecurityConfig { @Bean public AuthenticationProvider authenticationProvider() { - Assert.notNull(this.myUserRepository, "myUserRepository cannot be null"); + Assert.notNull(this.myUserRepository); return new AuthenticationProvider() { @Override public boolean supports(Class authentication) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableCustomMethodSecurity.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableCustomMethodSecurity.java deleted file mode 100644 index 3853c0ad08..0000000000 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableCustomMethodSecurity.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.method.configuration; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.context.annotation.AdviceMode; -import org.springframework.core.annotation.AliasFor; - -/** - * @author Evgeniy Cheban - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -@EnableMethodSecurity -public @interface EnableCustomMethodSecurity { - - @AliasFor(annotation = EnableMethodSecurity.class, attribute = "proxyTargetClass") - boolean proxyTargetClass() default false; - - @AliasFor(annotation = EnableMethodSecurity.class, attribute = "mode") - AdviceMode mode() default AdviceMode.PROXY; - -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java index 7e049bed7c..b6668108e8 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java @@ -95,21 +95,6 @@ public class PrePostMethodSecurityConfigurationTests { @Autowired(required = false) BusinessService businessService; - @WithMockUser - @Test - public void customMethodSecurityPreAuthorizeAdminWhenRoleUserThenAccessDeniedException() { - this.spring.register(CustomMethodSecurityServiceConfig.class).autowire(); - assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorizeAdmin) - .withMessage("Access Denied"); - } - - @WithMockUser(roles = "ADMIN") - @Test - public void customMethodSecurityPreAuthorizeAdminWhenRoleAdminThenPasses() { - this.spring.register(CustomMethodSecurityServiceConfig.class).autowire(); - this.methodSecurityService.preAuthorizeAdmin(); - } - @WithMockUser(roles = "ADMIN") @Test public void preAuthorizeWhenRoleAdminThenAccessDeniedException() { @@ -451,17 +436,6 @@ public class PrePostMethodSecurityConfigurationTests { return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false); } - @Configuration - @EnableCustomMethodSecurity - static class CustomMethodSecurityServiceConfig { - - @Bean - MethodSecurityService methodSecurityService() { - return new MethodSecurityServiceImpl(); - } - - } - @Configuration @EnableMethodSecurity static class MethodSecurityServiceConfig { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java index 9c2c1f0a14..2d49ddb242 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -21,7 +21,6 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurer; @@ -150,19 +149,6 @@ public class AbstractConfiguredSecurityBuilderTests { assertThat(builder.getConfigurers(DelegateSecurityConfigurer.class)).hasSize(2); } - @Test - public void withWhenConfigurerThenConfigurerAdded() throws Exception { - this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults()); - assertThat(this.builder.getConfigurers(TestSecurityConfigurer.class)).hasSize(1); - } - - @Test - public void withWhenDuplicateConfigurerAddedThenDuplicateConfigurerRemoved() throws Exception { - this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults()); - this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults()); - assertThat(this.builder.getConfigurers(TestSecurityConfigurer.class)).hasSize(1); - } - private static class ApplyAndRemoveSecurityConfigurer extends SecurityConfigurerAdapter { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryNoMvcTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryNoMvcTests.java index ec39518324..38ef3176e0 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryNoMvcTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryNoMvcTests.java @@ -25,12 +25,8 @@ import org.springframework.http.HttpMethod; import org.springframework.security.test.support.ClassPathExclusions; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.web.context.WebApplicationContext; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; /** * Tests for {@link AbstractRequestMatcherRegistry} with no Spring MVC in the classpath @@ -45,16 +41,13 @@ public class AbstractRequestMatcherRegistryNoMvcTests { @BeforeEach public void setUp() { this.matcherRegistry = new TestRequestMatcherRegistry(); - WebApplicationContext context = mock(WebApplicationContext.class); - given(context.getBeanNamesForType((Class) any())).willReturn(new String[0]); - this.matcherRegistry.setApplicationContext(context); } @Test public void requestMatchersWhenPatternAndMvcNotPresentThenReturnAntPathRequestMatcherType() { List requestMatchers = this.matcherRegistry.requestMatchers("/path"); assertThat(requestMatchers).isNotEmpty(); - assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.size()).isEqualTo(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class); } @@ -62,7 +55,7 @@ public class AbstractRequestMatcherRegistryNoMvcTests { public void requestMatchersWhenHttpMethodAndPatternAndMvcNotPresentThenReturnAntPathRequestMatcherType() { List requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET, "/path"); assertThat(requestMatchers).isNotEmpty(); - assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.size()).isEqualTo(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class); } @@ -70,7 +63,7 @@ public class AbstractRequestMatcherRegistryNoMvcTests { public void requestMatchersWhenHttpMethodAndMvcNotPresentThenReturnAntPathMatcherType() { List requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET); assertThat(requestMatchers).isNotEmpty(); - assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.size()).isEqualTo(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java index ddcddcd93c..3aa9df13b9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,11 +26,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpMethod; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.config.MockServletContext; -import org.springframework.security.config.TestMockHttpServletMappings; import org.springframework.security.config.annotation.ObjectPostProcessor; -import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.DispatcherServletDelegatingRequestMatcher; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher; @@ -43,9 +40,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link AbstractRequestMatcherRegistry}. @@ -81,7 +75,7 @@ public class AbstractRequestMatcherRegistryTests { List requestMatchers = this.matcherRegistry .requestMatchers(new RegexRequestMatcher("/a.*", HttpMethod.GET.name())); assertThat(requestMatchers).isNotEmpty(); - assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.size()).isEqualTo(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(RegexRequestMatcher.class); } @@ -90,7 +84,7 @@ public class AbstractRequestMatcherRegistryTests { List requestMatchers = this.matcherRegistry .requestMatchers(new RegexRequestMatcher("/a.*", null)); assertThat(requestMatchers).isNotEmpty(); - assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.size()).isEqualTo(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(RegexRequestMatcher.class); } @@ -99,7 +93,7 @@ public class AbstractRequestMatcherRegistryTests { List requestMatchers = this.matcherRegistry .requestMatchers(new AntPathRequestMatcher("/a.*", HttpMethod.GET.name())); assertThat(requestMatchers).isNotEmpty(); - assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.size()).isEqualTo(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class); } @@ -107,7 +101,7 @@ public class AbstractRequestMatcherRegistryTests { public void antMatchersWhenPatternParamThenReturnAntPathRequestMatcherType() { List requestMatchers = this.matcherRegistry.requestMatchers(new AntPathRequestMatcher("/a.*")); assertThat(requestMatchers).isNotEmpty(); - assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.size()).isEqualTo(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class); } @@ -116,7 +110,7 @@ public class AbstractRequestMatcherRegistryTests { List requestMatchers = this.matcherRegistry.dispatcherTypeMatchers(HttpMethod.GET, DispatcherType.ASYNC); assertThat(requestMatchers).isNotEmpty(); - assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.size()).isEqualTo(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(DispatcherTypeRequestMatcher.class); } @@ -124,7 +118,7 @@ public class AbstractRequestMatcherRegistryTests { public void dispatcherMatchersWhenPatternParamThenReturnAntPathRequestMatcherType() { List requestMatchers = this.matcherRegistry.dispatcherTypeMatchers(DispatcherType.INCLUDE); assertThat(requestMatchers).isNotEmpty(); - assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.size()).isEqualTo(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(DispatcherTypeRequestMatcher.class); } @@ -132,7 +126,7 @@ public class AbstractRequestMatcherRegistryTests { public void requestMatchersWhenPatternAndMvcPresentThenReturnMvcRequestMatcherType() { List requestMatchers = this.matcherRegistry.requestMatchers("/path"); assertThat(requestMatchers).isNotEmpty(); - assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.size()).isEqualTo(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(MvcRequestMatcher.class); } @@ -140,7 +134,7 @@ public class AbstractRequestMatcherRegistryTests { public void requestMatchersWhenHttpMethodAndPatternAndMvcPresentThenReturnMvcRequestMatcherType() { List requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET, "/path"); assertThat(requestMatchers).isNotEmpty(); - assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.size()).isEqualTo(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(MvcRequestMatcher.class); } @@ -148,7 +142,7 @@ public class AbstractRequestMatcherRegistryTests { public void requestMatchersWhenHttpMethodAndMvcPresentThenReturnMvcRequestMatcherType() { List requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET); assertThat(requestMatchers).isNotEmpty(); - assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.size()).isEqualTo(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(MvcRequestMatcher.class); } @@ -165,12 +159,16 @@ public class AbstractRequestMatcherRegistryTests { public void requestMatchersWhenNoDispatcherServletThenAntPathRequestMatcherType() { MockServletContext servletContext = new MockServletContext(); given(this.context.getServletContext()).willReturn(servletContext); - servletContext.addServlet("servletOne", Servlet.class).addMapping("/one"); - servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two"); List requestMatchers = this.matcherRegistry.requestMatchers("/**"); assertThat(requestMatchers).isNotEmpty(); assertThat(requestMatchers).hasSize(1); assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class); + servletContext.addServlet("servletOne", Servlet.class); + servletContext.addServlet("servletTwo", Servlet.class); + requestMatchers = this.matcherRegistry.requestMatchers("/**"); + assertThat(requestMatchers).isNotEmpty(); + assertThat(requestMatchers).hasSize(1); + assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class); } @Test @@ -178,26 +176,7 @@ public class AbstractRequestMatcherRegistryTests { MockServletContext servletContext = new MockServletContext(); given(this.context.getServletContext()).willReturn(servletContext); servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/"); - servletContext.addServlet("servletTwo", DispatcherServlet.class).addMapping("/servlet/*"); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> this.matcherRegistry.requestMatchers("/**")); - } - - @Test - public void requestMatchersWhenMultipleDispatcherServletMappingsThenException() { - MockServletContext servletContext = new MockServletContext(); - given(this.context.getServletContext()).willReturn(servletContext); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/mvc/*"); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> this.matcherRegistry.requestMatchers("/**")); - } - - @Test - public void requestMatchersWhenPathDispatcherServletAndOtherServletsThenException() { - MockServletContext servletContext = new MockServletContext(); - given(this.context.getServletContext()).willReturn(servletContext); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - servletContext.addServlet("default", Servlet.class).addMapping("/"); + servletContext.addServlet("servletTwo", Servlet.class).addMapping("/servlet/**"); assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> this.matcherRegistry.requestMatchers("/**")); } @@ -214,67 +193,6 @@ public class AbstractRequestMatcherRegistryTests { assertThat(requestMatchers.get(0)).isInstanceOf(MvcRequestMatcher.class); } - @Test - public void requestMatchersWhenOnlyDispatcherServletThenAllows() { - MockServletContext servletContext = new MockServletContext(); - given(this.context.getServletContext()).willReturn(servletContext); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - List requestMatchers = this.matcherRegistry.requestMatchers("/**"); - assertThat(requestMatchers).hasSize(1); - assertThat(requestMatchers.get(0)).isInstanceOf(MvcRequestMatcher.class); - } - - @Test - public void requestMatchersWhenImplicitServletsThenAllows() { - mockMvcIntrospector(true); - MockServletContext servletContext = new MockServletContext(); - given(this.context.getServletContext()).willReturn(servletContext); - servletContext.addServlet("defaultServlet", Servlet.class); - servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx"); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/"); - List requestMatchers = this.matcherRegistry.requestMatchers("/**"); - assertThat(requestMatchers).hasSize(1); - assertThat(requestMatchers.get(0)).isInstanceOf(DispatcherServletDelegatingRequestMatcher.class); - } - - @Test - public void requestMatchersWhenPathBasedNonDispatcherServletThenAllows() { - MockServletContext servletContext = new MockServletContext(); - given(this.context.getServletContext()).willReturn(servletContext); - servletContext.addServlet("path", Servlet.class).addMapping("/services/*"); - servletContext.addServlet("default", DispatcherServlet.class).addMapping("/"); - List requestMatchers = this.matcherRegistry.requestMatchers("/services/*"); - assertThat(requestMatchers).hasSize(1); - assertThat(requestMatchers.get(0)).isInstanceOf(DispatcherServletDelegatingRequestMatcher.class); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint"); - request.setHttpServletMapping(TestMockHttpServletMappings.defaultMapping()); - assertThat(requestMatchers.get(0).matcher(request).isMatch()).isTrue(); - request.setHttpServletMapping(TestMockHttpServletMappings.path(request, "/services")); - request.setServletPath("/services"); - request.setPathInfo("/endpoint"); - assertThat(requestMatchers.get(0).matcher(request).isMatch()).isTrue(); - } - - @Test - public void matchesWhenDispatcherServletThenMvc() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", DispatcherServlet.class).addMapping("/"); - servletContext.addServlet("path", Servlet.class).addMapping("/services/*"); - MvcRequestMatcher mvc = mock(MvcRequestMatcher.class); - AntPathRequestMatcher ant = mock(AntPathRequestMatcher.class); - DispatcherServletDelegatingRequestMatcher requestMatcher = new DispatcherServletDelegatingRequestMatcher(ant, - mvc, servletContext); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/endpoint"); - request.setHttpServletMapping(TestMockHttpServletMappings.defaultMapping()); - assertThat(requestMatcher.matches(request)).isFalse(); - verify(mvc).matches(request); - verifyNoInteractions(ant); - request.setHttpServletMapping(TestMockHttpServletMappings.path(request, "/services")); - assertThat(requestMatcher.matches(request)).isFalse(); - verify(ant).matches(request); - verifyNoMoreInteractions(mvc); - } - private void mockMvcIntrospector(boolean isPresent) { ApplicationContext context = this.matcherRegistry.getApplicationContext(); given(context.containsBean("mvcHandlerMappingIntrospector")).willReturn(isPresent); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/HttpConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/HttpConfigurationTests.java index e7326ab9f8..9c1abada6d 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/builders/HttpConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/HttpConfigurationTests.java @@ -20,8 +20,6 @@ import java.io.IOException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; @@ -31,7 +29,6 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.cas.web.CasAuthenticationFilter; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; @@ -45,9 +42,6 @@ import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -74,16 +68,6 @@ public class HttpConfigurationTests { + " Consider using addFilterBefore or addFilterAfter instead."); } - // https://github.com/spring-projects/spring-security-javaconfig/issues/104 - @Test - public void configureWhenAddFilterCasAuthenticationFilterThenFilterAdded() throws Exception { - CasAuthenticationFilterConfig.CAS_AUTHENTICATION_FILTER = spy(new CasAuthenticationFilter()); - this.spring.register(CasAuthenticationFilterConfig.class).autowire(); - this.mockMvc.perform(get("/")); - verify(CasAuthenticationFilterConfig.CAS_AUTHENTICATION_FILTER).doFilter(any(ServletRequest.class), - any(ServletResponse.class), any(FilterChain.class)); - } - @Test public void configureWhenConfigIsRequestMatchersJavadocThenAuthorizationApplied() throws Exception { this.spring.register(RequestMatcherRegistryConfigs.class).autowire(); @@ -123,22 +107,6 @@ public class HttpConfigurationTests { } - @EnableWebSecurity - static class CasAuthenticationFilterConfig { - - @Bean - SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - // @formatter:off - http - .addFilter(CAS_AUTHENTICATION_FILTER); - // @formatter:on - return http.build(); - } - - static CasAuthenticationFilter CAS_AUTHENTICATION_FILTER; - - } - @Configuration @EnableWebSecurity @EnableWebMvc diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java index 2d95161fc6..94a775dc82 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,6 @@ import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.event.AbstractAuthenticationEvent; import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; import org.springframework.security.authentication.event.AuthenticationSuccessEvent; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -64,11 +63,9 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.test.web.servlet.RequestCacheResultMatcher; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter; -import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; @@ -76,10 +73,6 @@ import org.springframework.web.accept.ContentNegotiationStrategy; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.filter.CorsFilter; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -92,8 +85,6 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; -import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; -import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -361,40 +352,6 @@ public class HttpSecurityConfigurationTests { DefaultLogoutPageGeneratingFilter.class); } - @Test - public void configureWhenCorsConfigurationSourceThenApplyCors() { - this.spring.register(CorsConfigurationSourceConfig.class, DefaultWithFilterChainConfig.class).autowire(); - SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class); - CorsFilter corsFilter = (CorsFilter) filterChain.getFilters() - .stream() - .filter((f) -> f instanceof CorsFilter) - .findFirst() - .get(); - Object configSource = ReflectionTestUtils.getField(corsFilter, "configSource"); - assertThat(configSource).isInstanceOf(UrlBasedCorsConfigurationSource.class); - } - - @Test - public void configureWhenAddingCustomDslUsingWithThenApplied() throws Exception { - this.spring.register(WithCustomDslConfig.class, UserDetailsConfig.class).autowire(); - SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class); - List filters = filterChain.getFilters(); - assertThat(filters).hasAtLeastOneElementOfType(UsernamePasswordAuthenticationFilter.class); - this.mockMvc.perform(formLogin()).andExpectAll(redirectedUrl("/"), authenticated()); - } - - @Test - public void configureWhenCustomDslAddedFromFactoriesAndDisablingUsingWithThenNotApplied() throws Exception { - this.springFactoriesLoader - .when(() -> SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, getClass().getClassLoader())) - .thenReturn(List.of(new WithCustomDsl())); - this.spring.register(WithCustomDslDisabledConfig.class, UserDetailsConfig.class).autowire(); - SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class); - List filters = filterChain.getFilters(); - assertThat(filters).doesNotHaveAnyElementsOfTypes(UsernamePasswordAuthenticationFilter.class); - this.mockMvc.perform(formLogin()).andExpectAll(status().isNotFound(), unauthenticated()); - } - @RestController static class NameController { @@ -659,20 +616,6 @@ public class HttpSecurityConfigurationTests { } - @Configuration - static class CorsConfigurationSourceConfig { - - @Bean - CorsConfigurationSource corsConfigurationSource() { - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - CorsConfiguration corsConfiguration = new CorsConfiguration(); - corsConfiguration.setAllowedOrigins(List.of("http://localhost:8080")); - source.registerCorsConfiguration("/**", corsConfiguration); - return source; - } - - } - static class DefaultConfigurer extends AbstractHttpConfigurer { boolean init; @@ -691,45 +634,4 @@ public class HttpSecurityConfigurationTests { } - @Configuration - @EnableWebSecurity - static class WithCustomDslConfig { - - @Bean - SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - // @formatter:off - http - .with(new WithCustomDsl(), Customizer.withDefaults()) - .httpBasic(Customizer.withDefaults()); - // @formatter:on - return http.build(); - } - - } - - @Configuration - @EnableWebSecurity - static class WithCustomDslDisabledConfig { - - @Bean - SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - // @formatter:off - http - .with(new WithCustomDsl(), (dsl) -> dsl.disable()) - .httpBasic(Customizer.withDefaults()); - // @formatter:on - return http.build(); - } - - } - - static class WithCustomDsl extends AbstractHttpConfigurer { - - @Override - public void init(HttpSecurity builder) throws Exception { - builder.formLogin(Customizer.withDefaults()); - } - - } - } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2AuthorizedClientManagerConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2AuthorizedClientManagerConfigurationTests.java deleted file mode 100644 index 45d69c2323..0000000000 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2AuthorizedClientManagerConfigurationTests.java +++ /dev/null @@ -1,547 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configuration; - -import java.time.Duration; -import java.time.Instant; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.function.Consumer; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; -import org.springframework.security.config.test.SpringTestContext; -import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; -import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider; -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.OAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest; -import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; -import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; -import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -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.TestOAuth2RefreshTokens; -import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.security.oauth2.jwt.JoseHeaderNames; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtClaimNames; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.util.StringUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link OAuth2ClientConfiguration.OAuth2AuthorizedClientManagerConfiguration}. - * - * @author Joe Grandja - * @author Steve Riesenberg - */ -public class OAuth2AuthorizedClientManagerConfigurationTests { - - private static OAuth2AccessTokenResponseClient MOCK_RESPONSE_CLIENT; - - public final SpringTestContext spring = new SpringTestContext(this); - - @Autowired - private OAuth2AuthorizedClientManager authorizedClientManager; - - @Autowired - private ClientRegistrationRepository clientRegistrationRepository; - - @Autowired - private OAuth2AuthorizedClientRepository authorizedClientRepository; - - @Autowired(required = false) - private AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider; - - private MockHttpServletRequest request; - - private MockHttpServletResponse response; - - @BeforeEach - @SuppressWarnings("unchecked") - public void setUp() { - MOCK_RESPONSE_CLIENT = mock(OAuth2AccessTokenResponseClient.class); - this.request = new MockHttpServletRequest(); - this.response = new MockHttpServletResponse(); - } - - @Test - public void loadContextWhenOAuth2ClientEnabledThenConfigured() { - this.spring.register(MinimalOAuth2ClientConfig.class).autowire(); - assertThat(this.authorizedClientManager).isNotNull(); - } - - @Test - public void authorizeWhenAuthorizationCodeAuthorizedClientProviderBeanThenUsed() { - this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire(); - - TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null); - // @formatter:off - OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest - .withClientRegistrationId("google") - .principal(authentication) - .attribute(HttpServletRequest.class.getName(), this.request) - .attribute(HttpServletResponse.class.getName(), this.response) - .build(); - assertThatExceptionOfType(ClientAuthorizationRequiredException.class) - .isThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest)) - .extracting(OAuth2AuthorizationException::getError) - .extracting(OAuth2Error::getErrorCode) - .isEqualTo("client_authorization_required"); - // @formatter:on - - verify(this.authorizationCodeAuthorizedClientProvider).authorize(any(OAuth2AuthorizationContext.class)); - } - - @Test - public void authorizeWhenRefreshTokenAccessTokenResponseClientBeanThenUsed() { - this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire(); - testRefreshTokenGrant(); - } - - @Test - public void authorizeWhenRefreshTokenAuthorizedClientProviderBeanThenUsed() { - this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire(); - testRefreshTokenGrant(); - } - - private void testRefreshTokenGrant() { - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); - given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class))) - .willReturn(accessTokenResponse); - - TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null); - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google"); - OAuth2AuthorizedClient existingAuthorizedClient = new OAuth2AuthorizedClient(clientRegistration, - authentication.getName(), getExpiredAccessToken(), TestOAuth2RefreshTokens.refreshToken()); - this.authorizedClientRepository.saveAuthorizedClient(existingAuthorizedClient, authentication, this.request, - this.response); - // @formatter:off - OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest - .withAuthorizedClient(existingAuthorizedClient) - .principal(authentication) - .attribute(HttpServletRequest.class.getName(), this.request) - .attribute(HttpServletResponse.class.getName(), this.response) - .build(); - // @formatter:on - OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest); - assertThat(authorizedClient).isNotNull(); - - ArgumentCaptor grantRequestCaptor = ArgumentCaptor - .forClass(OAuth2RefreshTokenGrantRequest.class); - verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); - - OAuth2RefreshTokenGrantRequest grantRequest = grantRequestCaptor.getValue(); - assertThat(grantRequest.getClientRegistration().getRegistrationId()) - .isEqualTo(clientRegistration.getRegistrationId()); - assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.REFRESH_TOKEN); - assertThat(grantRequest.getAccessToken()).isEqualTo(existingAuthorizedClient.getAccessToken()); - assertThat(grantRequest.getRefreshToken()).isEqualTo(existingAuthorizedClient.getRefreshToken()); - } - - @Test - public void authorizeWhenClientCredentialsAccessTokenResponseClientBeanThenUsed() { - this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire(); - testClientCredentialsGrant(); - } - - @Test - public void authorizeWhenClientCredentialsAuthorizedClientProviderBeanThenUsed() { - this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire(); - testClientCredentialsGrant(); - } - - private void testClientCredentialsGrant() { - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); - given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2ClientCredentialsGrantRequest.class))) - .willReturn(accessTokenResponse); - - TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null); - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("github"); - // @formatter:off - OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest - .withClientRegistrationId(clientRegistration.getRegistrationId()) - .principal(authentication) - .attribute(HttpServletRequest.class.getName(), this.request) - .attribute(HttpServletResponse.class.getName(), this.response) - .build(); - // @formatter:on - OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest); - assertThat(authorizedClient).isNotNull(); - - ArgumentCaptor grantRequestCaptor = ArgumentCaptor - .forClass(OAuth2ClientCredentialsGrantRequest.class); - verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); - - OAuth2ClientCredentialsGrantRequest grantRequest = grantRequestCaptor.getValue(); - assertThat(grantRequest.getClientRegistration().getRegistrationId()) - .isEqualTo(clientRegistration.getRegistrationId()); - assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS); - } - - @Test - public void authorizeWhenPasswordAccessTokenResponseClientBeanThenUsed() { - this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire(); - testPasswordGrant(); - } - - @Test - public void authorizeWhenPasswordAuthorizedClientProviderBeanThenUsed() { - this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire(); - testPasswordGrant(); - } - - private void testPasswordGrant() { - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); - given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2PasswordGrantRequest.class))) - .willReturn(accessTokenResponse); - - TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password"); - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("facebook"); - // @formatter:off - OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest - .withClientRegistrationId(clientRegistration.getRegistrationId()) - .principal(authentication) - .attribute(HttpServletRequest.class.getName(), this.request) - .attribute(HttpServletResponse.class.getName(), this.response) - .build(); - // @formatter:on - this.request.setParameter(OAuth2ParameterNames.USERNAME, "user"); - this.request.setParameter(OAuth2ParameterNames.PASSWORD, "password"); - OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest); - assertThat(authorizedClient).isNotNull(); - - ArgumentCaptor grantRequestCaptor = ArgumentCaptor - .forClass(OAuth2PasswordGrantRequest.class); - verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); - - OAuth2PasswordGrantRequest grantRequest = grantRequestCaptor.getValue(); - assertThat(grantRequest.getClientRegistration().getRegistrationId()) - .isEqualTo(clientRegistration.getRegistrationId()); - assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.PASSWORD); - assertThat(grantRequest.getUsername()).isEqualTo("user"); - assertThat(grantRequest.getPassword()).isEqualTo("password"); - } - - @Test - public void authorizeWhenJwtBearerAccessTokenResponseClientBeanThenUsed() { - this.spring.register(CustomAccessTokenResponseClientsConfig.class).autowire(); - testJwtBearerGrant(); - } - - @Test - public void authorizeWhenJwtBearerAuthorizedClientProviderBeanThenUsed() { - this.spring.register(CustomAuthorizedClientProvidersConfig.class).autowire(); - testJwtBearerGrant(); - } - - private void testJwtBearerGrant() { - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); - given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(JwtBearerGrantRequest.class))).willReturn(accessTokenResponse); - - JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt()); - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("okta"); - // @formatter:off - OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest - .withClientRegistrationId(clientRegistration.getRegistrationId()) - .principal(authentication) - .attribute(HttpServletRequest.class.getName(), this.request) - .attribute(HttpServletResponse.class.getName(), this.response) - .build(); - // @formatter:on - OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest); - assertThat(authorizedClient).isNotNull(); - - ArgumentCaptor grantRequestCaptor = ArgumentCaptor.forClass(JwtBearerGrantRequest.class); - verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); - - JwtBearerGrantRequest grantRequest = grantRequestCaptor.getValue(); - assertThat(grantRequest.getClientRegistration().getRegistrationId()) - .isEqualTo(clientRegistration.getRegistrationId()); - assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER); - assertThat(grantRequest.getJwt().getSubject()).isEqualTo("user"); - } - - private static OAuth2AccessToken getExpiredAccessToken() { - Instant expiresAt = Instant.now().minusSeconds(60); - Instant issuedAt = expiresAt.minus(Duration.ofDays(1)); - return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "scopes", issuedAt, expiresAt, - new HashSet<>(Arrays.asList("read", "write"))); - } - - private static Jwt getJwt() { - Instant issuedAt = Instant.now(); - return new Jwt("token", issuedAt, issuedAt.plusSeconds(300), - Collections.singletonMap(JoseHeaderNames.ALG, "RS256"), - Collections.singletonMap(JwtClaimNames.SUB, "user")); - } - - @Configuration - @EnableWebSecurity - static class MinimalOAuth2ClientConfig extends OAuth2ClientBaseConfig { - - } - - @Configuration - @EnableWebSecurity - static class CustomAccessTokenResponseClientsConfig extends OAuth2ClientBaseConfig { - - @Bean - OAuth2AccessTokenResponseClient authorizationCodeTokenResponseClient() { - return new MockAuthorizationCodeClient(); - } - - @Bean - OAuth2AccessTokenResponseClient refreshTokenTokenResponseClient() { - return new MockRefreshTokenClient(); - } - - @Bean - OAuth2AccessTokenResponseClient clientCredentialsTokenResponseClient() { - return new MockClientCredentialsClient(); - } - - @Bean - OAuth2AccessTokenResponseClient passwordTokenResponseClient() { - return new MockPasswordClient(); - } - - @Bean - OAuth2AccessTokenResponseClient jwtBearerTokenResponseClient() { - return new MockJwtBearerClient(); - } - - @Bean - OAuth2UserService oauth2UserService() { - return mock(DefaultOAuth2UserService.class); - } - - @Bean - OAuth2UserService oidcUserService() { - return mock(OidcUserService.class); - } - - } - - @Configuration - @EnableWebSecurity - static class CustomAuthorizedClientProvidersConfig extends OAuth2ClientBaseConfig { - - @Bean - AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeProvider() { - return spy(new AuthorizationCodeOAuth2AuthorizedClientProvider()); - } - - @Bean - RefreshTokenOAuth2AuthorizedClientProvider refreshTokenProvider() { - RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(new MockRefreshTokenClient()); - return authorizedClientProvider; - } - - @Bean - ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsProvider() { - ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(new MockClientCredentialsClient()); - return authorizedClientProvider; - } - - @Bean - PasswordOAuth2AuthorizedClientProvider passwordProvider() { - PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(new MockPasswordClient()); - return authorizedClientProvider; - } - - @Bean - JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider() { - JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(new MockJwtBearerClient()); - return authorizedClientProvider; - } - - } - - abstract static class OAuth2ClientBaseConfig { - - @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - // @formatter:off - http - .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) - .oauth2Login(Customizer.withDefaults()) - .oauth2Client(Customizer.withDefaults()); - return http.build(); - // @formatter:on - } - - @Bean - ClientRegistrationRepository clientRegistrationRepository() { - // @formatter:off - return new InMemoryClientRegistrationRepository(Arrays.asList( - CommonOAuth2Provider.GOOGLE.getBuilder("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .build(), - CommonOAuth2Provider.GITHUB.getBuilder("github") - .clientId("github-client-id") - .clientSecret("github-client-secret") - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) - .build(), - CommonOAuth2Provider.FACEBOOK.getBuilder("facebook") - .clientId("facebook-client-id") - .clientSecret("facebook-client-secret") - .authorizationGrantType(AuthorizationGrantType.PASSWORD) - .build(), - CommonOAuth2Provider.OKTA.getBuilder("okta") - .clientId("okta-client-id") - .clientSecret("okta-client-secret") - .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) - .build())); - // @formatter:on - } - - @Bean - OAuth2AuthorizedClientRepository authorizedClientRepository() { - return mock(OAuth2AuthorizedClientRepository.class); - } - - @Bean - Consumer authorizedClientManagerConsumer() { - return (authorizedClientManager) -> authorizedClientManager - .setContextAttributesMapper((authorizeRequest) -> { - HttpServletRequest request = Objects - .requireNonNull(authorizeRequest.getAttribute(HttpServletRequest.class.getName())); - String username = request.getParameter(OAuth2ParameterNames.USERNAME); - String password = request.getParameter(OAuth2ParameterNames.PASSWORD); - - Map attributes = Collections.emptyMap(); - if (StringUtils.hasText(username) && StringUtils.hasText(password)) { - attributes = new HashMap<>(); - attributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); - attributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); - } - - return attributes; - }); - } - - } - - private static class MockAuthorizationCodeClient - implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse( - OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockRefreshTokenClient - implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockClientCredentialsClient - implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse( - OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockPasswordClient implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockJwtBearerClient implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(JwtBearerGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java index 83a247673a..aea25c75e0 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java @@ -180,10 +180,9 @@ public class OAuth2ClientConfigurationTests { @Test public void loadContextWhenAccessTokenResponseClientRegisteredTwiceThenThrowNoUniqueBeanDefinitionException() { // @formatter:off - assertThatExceptionOfType(BeanCreationException.class) + assertThatExceptionOfType(Exception.class) .isThrownBy(() -> this.spring.register(AccessTokenResponseClientRegisteredTwiceConfig.class).autowire()) - .havingRootCause() - .isInstanceOf(NoUniqueBeanDefinitionException.class) + .withRootCauseInstanceOf(NoUniqueBeanDefinitionException.class) .withMessageContaining( "expected single matching bean but found 2: accessTokenResponseClient1,accessTokenResponseClient2"); // @formatter:on diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfigurationTests.java index 7dac892969..5be3bfe071 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,20 +17,14 @@ package org.springframework.security.config.annotation.web.configuration; import java.net.URI; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadFactory; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import reactor.core.CoreSubscriber; import reactor.core.publisher.BaseSubscriber; @@ -41,8 +35,6 @@ import reactor.util.context.Context; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletRequest; @@ -54,7 +46,6 @@ import org.springframework.security.config.annotation.web.configuration.Security import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.oauth2.client.web.reactive.function.client.MockExchangeFunction; @@ -280,58 +271,6 @@ public class SecurityReactorContextConfigurationTests { verify(strategy, times(2)).getContext(); } - @Test - public void createPublisherWhenThreadFactoryIsPlatformThenSecurityContextAttributesAvailable() throws Exception { - this.spring.register(SecurityConfig.class).autowire(); - - ThreadFactory threadFactory = Executors.defaultThreadFactory(); - assertContextAttributesAvailable(threadFactory); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void createPublisherWhenThreadFactoryIsVirtualThenSecurityContextAttributesAvailable() throws Exception { - this.spring.register(SecurityConfig.class).autowire(); - - ThreadFactory threadFactory = new VirtualThreadTaskExecutor().getVirtualThreadFactory(); - assertContextAttributesAvailable(threadFactory); - } - - private void assertContextAttributesAvailable(ThreadFactory threadFactory) throws Exception { - Map expectedContextAttributes = new HashMap<>(); - expectedContextAttributes.put(HttpServletRequest.class, this.servletRequest); - expectedContextAttributes.put(HttpServletResponse.class, this.servletResponse); - expectedContextAttributes.put(Authentication.class, this.authentication); - - try (SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(threadFactory)) { - Future> future = taskExecutor.submit(this::propagateRequestAttributes); - assertThat(future.get()).isEqualTo(expectedContextAttributes); - } - } - - private Map propagateRequestAttributes() { - RequestAttributes requestAttributes = new ServletRequestAttributes(this.servletRequest, this.servletResponse); - RequestContextHolder.setRequestAttributes(requestAttributes); - - SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(this.authentication); - SecurityContextHolder.setContext(securityContext); - - // @formatter:off - return Mono.deferContextual(Mono::just) - .filter((ctx) -> ctx.hasKey(SecurityReactorContextSubscriber.SECURITY_CONTEXT_ATTRIBUTES)) - .map((ctx) -> ctx.>get(SecurityReactorContextSubscriber.SECURITY_CONTEXT_ATTRIBUTES)) - .map((attributes) -> { - Map map = new HashMap<>(); - // Copy over items from lazily loaded map - Arrays.asList(HttpServletRequest.class, HttpServletResponse.class, Authentication.class) - .forEach((key) -> map.put(key, attributes.get(key))); - return map; - }) - .block(); - // @formatter:on - } - @Configuration @EnableWebSecurity static class SecurityConfig { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherBuilderRegistryTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherBuilderRegistryTests.java deleted file mode 100644 index ef29d6a86c..0000000000 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherBuilderRegistryTests.java +++ /dev/null @@ -1,349 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers; - -import java.util.List; -import java.util.function.Consumer; - -import jakarta.servlet.Servlet; -import jakarta.servlet.ServletContext; -import org.assertj.core.api.AbstractObjectAssert; -import org.assertj.core.api.ObjectAssert; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpMethod; -import org.springframework.security.config.MockServletContext; -import org.springframework.security.config.annotation.ObjectPostProcessor; -import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.security.web.util.matcher.AndRequestMatcher; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.context.support.GenericWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link AbstractRequestMatcherBuilderRegistry} - */ -class AbstractRequestMatcherBuilderRegistryTests { - - @Test - void defaultServletMatchersWhenDefaultDispatcherServletThenMvc() { - MockServletContext servletContext = MockServletContext.mvc(); - List matchers = defaultServlet(servletContext).requestMatchers("/mvc").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isNull(); - assertThatMvc(matchers).pattern().isEqualTo("/mvc"); - assertThatMvc(matchers).method().isNull(); - } - - @Test - void defaultServletHttpMethodMatchersWhenDefaultDispatcherServletThenMvc() { - MockServletContext servletContext = MockServletContext.mvc(); - List matchers = defaultServlet(servletContext).requestMatchers(HttpMethod.GET, "/mvc").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isNull(); - assertThatMvc(matchers).pattern().isEqualTo("/mvc"); - assertThatMvc(matchers).method().isEqualTo(HttpMethod.GET); - } - - @Test - void servletMatchersWhenPathDispatcherServletThenMvc() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - List matchers = servletPattern(servletContext, "/mvc/*") - .requestMatchers("/controller").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isEqualTo("/mvc"); - assertThatMvc(matchers).pattern().isEqualTo("/controller"); - } - - @Test - void servletMatchersWhenAlsoExtraServletContainerMappingsThenMvc() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", Servlet.class); - servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx"); - servletContext.addServlet("facesServlet", Servlet.class).addMapping("/faces/", "*.jsf", "*.faces", "*.xhtml"); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - List matchers = servletPattern(servletContext, "/mvc/*") - .requestMatchers("/controller").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isEqualTo("/mvc"); - assertThatMvc(matchers).pattern().isEqualTo("/controller"); - } - - @Test - void defaultServletMatchersWhenOnlyDefaultServletThenAnt() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", Servlet.class).addMapping("/"); - List matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class); - assertThatAnt(matchers).pattern().isEqualTo("/controller"); - } - - @Test - void defaultDispatcherServletMatchersWhenNoHandlerMappingIntrospectorThenException() { - MockServletContext servletContext = MockServletContext.mvc(); - assertThatExceptionOfType(NoSuchBeanDefinitionException.class) - .isThrownBy(() -> defaultServlet(servletContext, (context) -> { - })); - } - - @Test - void dispatcherServletMatchersWhenNoHandlerMappingIntrospectorThenException() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - assertThatExceptionOfType(NoSuchBeanDefinitionException.class) - .isThrownBy(() -> servletPattern(servletContext, (context) -> { - }, "/mvc/*")); - } - - @Test - void matchersWhenNoDispatchServletThenAnt() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", Servlet.class).addMapping("/"); - servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*"); - List matchers = defaultServlet(servletContext).requestMatchers("/services/endpoint").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class); - assertThatAnt(matchers).pattern().isEqualTo("/services/endpoint"); - } - - @Test - void servletMatchersWhenMixedServletsThenDeterminesByServletPath() { - MockServletContext servletContext = MockServletContext.mvc(); - servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*"); - List matchers = servletPattern(servletContext, "/services/*") - .requestMatchers("/endpoint").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class); - assertThatAnt(matchers).pattern().isEqualTo("/services/endpoint"); - matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isNull(); - assertThatMvc(matchers).pattern().isEqualTo("/controller"); - } - - @Test - void servletMatchersWhenDispatcherServletNotDefaultThenDeterminesByServletPath() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", Servlet.class).addMapping("/"); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - List matchers = servletPattern(servletContext, "/mvc/*") - .requestMatchers("/controller").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isEqualTo("/mvc"); - assertThatMvc(matchers).pattern().isEqualTo("/controller"); - matchers = defaultServlet(servletContext).requestMatchers("/endpoint").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class); - assertThatAnt(matchers).pattern().isEqualTo("/endpoint"); - } - - @Test - void servletHttpMatchersWhenDispatcherServletNotDefaultThenDeterminesByServletPath() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", Servlet.class).addMapping("/"); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - List matchers = servletPattern(servletContext, "/mvc/*").requestMatchers(HttpMethod.GET, - "/controller").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).method().isEqualTo(HttpMethod.GET); - assertThatMvc(matchers).servletPath().isEqualTo("/mvc"); - assertThatMvc(matchers).pattern().isEqualTo("/controller"); - matchers = defaultServlet(servletContext).requestMatchers(HttpMethod.GET, "/endpoint").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class); - assertThatAnt(matchers).method().isEqualTo(HttpMethod.GET); - assertThatAnt(matchers).pattern().isEqualTo("/endpoint"); - } - - @Test - void servletMatchersWhenTwoDispatcherServletsThenDeterminesByServletPath() { - MockServletContext servletContext = MockServletContext.mvc(); - servletContext.addServlet("two", DispatcherServlet.class).addMapping("/other/*"); - List matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isNull(); - assertThatMvc(matchers).pattern().isEqualTo("/controller"); - matchers = servletPattern(servletContext, "/other/*").requestMatchers("/endpoint").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isEqualTo("/other"); - assertThatMvc(matchers).pattern().isEqualTo("/endpoint"); - } - - @Test - void servletMatchersWhenMoreThanOneMappingThenDeterminesByServletPath() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/two/*"); - List matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isNull(); - assertThatMvc(matchers).pattern().isEqualTo("/controller"); - matchers = servletPattern(servletContext, "/two/*").requestMatchers("/endpoint").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isEqualTo("/two"); - assertThatMvc(matchers).pattern().isEqualTo("/endpoint"); - } - - @Test - void servletMatchersWhenMoreThanOneMappingAndDefaultServletsThenDeterminesByServletPath() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/two/*"); - servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx"); - List matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isNull(); - assertThatMvc(matchers).pattern().isEqualTo("/controller"); - matchers = servletPattern(servletContext, "/two/*").requestMatchers("/endpoint").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isEqualTo("/two"); - assertThatMvc(matchers).pattern().isEqualTo("/endpoint"); - } - - @Test - void defaultServletWhenDispatcherServletThenMvc() { - MockServletContext servletContext = MockServletContext.mvc(); - servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*"); - List matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class); - assertThatMvc(matchers).servletPath().isNull(); - assertThatMvc(matchers).pattern().isEqualTo("/controller"); - matchers = servletPattern(servletContext, "/services/*").requestMatchers("/endpoint").matchers; - assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class); - assertThatAnt(matchers).pattern().isEqualTo("/services/endpoint"); - } - - @Test - void defaultServletWhenNoDefaultServletThenException() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*"); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> defaultServlet(servletContext)); - } - - @Test - void servletPathWhenNoMatchingServletThenException() { - MockServletContext servletContext = MockServletContext.mvc(); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> servletPattern(servletContext, "/wrong/*")); - } - - TestServletRequestMatcherRegistry defaultServlet(ServletContext servletContext) { - return servletPattern(servletContext, "/"); - } - - TestServletRequestMatcherRegistry defaultServlet(ServletContext servletContext, - Consumer consumer) { - return servletPattern(servletContext, consumer, "/"); - } - - TestServletRequestMatcherRegistry servletPattern(ServletContext servletContext, String pattern) { - return servletPattern(servletContext, (context) -> { - context.registerBean("mvcHandlerMappingIntrospector", HandlerMappingIntrospector.class); - context.registerBean(ObjectPostProcessor.class, () -> mock(ObjectPostProcessor.class)); - }, pattern); - } - - TestServletRequestMatcherRegistry servletPattern(ServletContext servletContext, - Consumer consumer, String pattern) { - GenericWebApplicationContext context = new GenericWebApplicationContext(servletContext); - consumer.accept(context); - context.refresh(); - return new TestServletRequestMatcherRegistry(context, pattern); - } - - static MvcRequestMatcherAssert assertThatMvc(List matchers) { - RequestMatcher matcher = matchers.get(0); - if (matcher instanceof AndRequestMatcher matching) { - List and = (List) ReflectionTestUtils.getField(matching, "requestMatchers"); - assertThat(and).hasSize(2); - assertThat(and.get(1)).isInstanceOf(MvcRequestMatcher.class); - return new MvcRequestMatcherAssert((MvcRequestMatcher) and.get(1)); - } - assertThat(matcher).isInstanceOf(MvcRequestMatcher.class); - return new MvcRequestMatcherAssert((MvcRequestMatcher) matcher); - } - - static AntPathRequestMatcherAssert assertThatAnt(List matchers) { - RequestMatcher matcher = matchers.get(0); - if (matcher instanceof AndRequestMatcher matching) { - List and = (List) ReflectionTestUtils.getField(matching, "requestMatchers"); - assertThat(and).hasSize(2); - assertThat(and.get(1)).isInstanceOf(AntPathRequestMatcher.class); - return new AntPathRequestMatcherAssert((AntPathRequestMatcher) and.get(1)); - } - assertThat(matcher).isInstanceOf(AntPathRequestMatcher.class); - return new AntPathRequestMatcherAssert((AntPathRequestMatcher) matcher); - } - - static final class TestServletRequestMatcherRegistry - extends AbstractRequestMatcherBuilderRegistry { - - List matchers; - - TestServletRequestMatcherRegistry(ApplicationContext context, String pattern) { - super(context, RequestMatcherBuilders.createForServletPattern(context, pattern)); - } - - @Override - protected TestServletRequestMatcherRegistry chainRequestMatchers(List requestMatchers) { - this.matchers = requestMatchers; - return this; - } - - } - - static final class MvcRequestMatcherAssert extends ObjectAssert { - - private MvcRequestMatcherAssert(MvcRequestMatcher matcher) { - super(matcher); - } - - AbstractObjectAssert servletPath() { - return extracting("servletPath"); - } - - AbstractObjectAssert pattern() { - return extracting("pattern"); - } - - AbstractObjectAssert method() { - return extracting("method"); - } - - } - - static final class AntPathRequestMatcherAssert extends ObjectAssert { - - private AntPathRequestMatcherAssert(AntPathRequestMatcher matcher) { - super(matcher); - } - - AbstractObjectAssert pattern() { - return extracting("pattern"); - } - - AbstractObjectAssert method() { - return extracting("httpMethod"); - } - - } - -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java index 19d5de6f24..cc3a311241 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ package org.springframework.security.config.annotation.web.configurers; import java.util.function.Supplier; -import jakarta.servlet.Servlet; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -27,21 +26,15 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.access.hierarchicalroles.RoleHierarchy; -import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.authentication.RememberMeAuthenticationToken; import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.config.MockServletContext; -import org.springframework.security.config.TestMockHttpServletMappings; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.authority.AuthorityUtils; @@ -62,7 +55,6 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; @@ -76,7 +68,6 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -127,7 +118,7 @@ public class AuthorizeHttpRequestsConfigurerTests { public void configureWhenMvcMatcherAfterAnyRequestThenException() { assertThatExceptionOfType(BeanCreationException.class) .isThrownBy(() -> this.spring.register(AfterAnyRequestConfig.class).autowire()) - .withMessageContaining("Can't configure requestMatchers after anyRequest"); + .withMessageContaining("Can't configure mvcMatchers after anyRequest"); } @Test @@ -282,17 +273,6 @@ public class AuthorizeHttpRequestsConfigurerTests { this.mvc.perform(requestWithAdmin).andExpect(status().isForbidden()); } - @Test - public void getWhenHasRoleUserAndRoleHierarchyConfiguredThenGreaterRoleTakesPrecedence() throws Exception { - this.spring.register(RoleHierarchyUserConfig.class, BasicController.class).autowire(); - // @formatter:off - MockHttpServletRequestBuilder requestWithAdmin = get("/") - .with(user("user") - .roles("ADMIN")); - // @formatter:on - this.mvc.perform(requestWithAdmin).andExpect(status().isOk()); - } - @Test public void getWhenRoleUserOrAdminConfiguredAndRoleIsUserThenRespondsWithOk() throws Exception { this.spring.register(RoleUserOrAdminConfig.class, BasicController.class).autowire(); @@ -368,7 +348,7 @@ public class AuthorizeHttpRequestsConfigurerTests { @Test public void getWhenServletPathRoleAdminConfiguredAndRoleIsUserThenRespondsWithForbidden() throws Exception { - this.spring.register(MvcServletPathConfig.class, BasicController.class).autowire(); + this.spring.register(ServletPathConfig.class, BasicController.class).autowire(); // @formatter:off MockHttpServletRequestBuilder requestWithUser = get("/spring/") .servletPath("/spring") @@ -381,7 +361,7 @@ public class AuthorizeHttpRequestsConfigurerTests { @Test public void getWhenServletPathRoleAdminConfiguredAndRoleIsUserAndWithoutServletPathThenRespondsWithForbidden() throws Exception { - this.spring.register(MvcServletPathConfig.class, BasicController.class).autowire(); + this.spring.register(ServletPathConfig.class, BasicController.class).autowire(); // @formatter:off MockHttpServletRequestBuilder requestWithUser = get("/") .with(user("user") @@ -392,7 +372,7 @@ public class AuthorizeHttpRequestsConfigurerTests { @Test public void getWhenServletPathRoleAdminConfiguredAndRoleIsAdminThenRespondsWithOk() throws Exception { - this.spring.register(MvcServletPathConfig.class, BasicController.class).autowire(); + this.spring.register(ServletPathConfig.class, BasicController.class).autowire(); // @formatter:off MockHttpServletRequestBuilder requestWithAdmin = get("/spring/") .servletPath("/spring") @@ -483,43 +463,6 @@ public class AuthorizeHttpRequestsConfigurerTests { this.mvc.perform(requestWithRoleOther).andExpect(status().isForbidden()); } - @Test - public void getWhenCustomRolePrefixAndRoleHasDifferentPrefixThenRespondsWithForbidden() throws Exception { - this.spring.register(GrantedAuthorityDefaultHasRoleConfig.class, BasicController.class).autowire(); - // @formatter:off - MockHttpServletRequestBuilder requestWithUser = get("/") - .with(user("user") - .authorities(new SimpleGrantedAuthority("ROLE_USER"))); - // @formatter:on - this.mvc.perform(requestWithUser).andExpect(status().isForbidden()); - } - - @Test - public void getWhenCustomRolePrefixAndHasRoleThenRespondsWithOk() throws Exception { - this.spring.register(GrantedAuthorityDefaultHasRoleConfig.class, BasicController.class).autowire(); - // @formatter:off - MockHttpServletRequestBuilder requestWithUser = get("/") - .with(user("user") - .authorities(new SimpleGrantedAuthority("CUSTOM_PREFIX_USER"))); - // @formatter:on - this.mvc.perform(requestWithUser).andExpect(status().isOk()); - } - - @Test - public void getWhenCustomRolePrefixAndHasAnyRoleThenRespondsWithOk() throws Exception { - this.spring.register(GrantedAuthorityDefaultHasAnyRoleConfig.class, BasicController.class).autowire(); - // @formatter:off - MockHttpServletRequestBuilder requestWithUser = get("/") - .with(user("user") - .authorities(new SimpleGrantedAuthority("CUSTOM_PREFIX_USER"))); - MockHttpServletRequestBuilder requestWithAdmin = get("/") - .with(user("user") - .authorities(new SimpleGrantedAuthority("CUSTOM_PREFIX_ADMIN"))); - // @formatter:on - this.mvc.perform(requestWithUser).andExpect(status().isOk()); - this.mvc.perform(requestWithAdmin).andExpect(status().isOk()); - } - @Test public void getWhenExpressionHasIpAddressLocalhostConfiguredIpAddressIsLocalhostThenRespondsWithOk() throws Exception { @@ -602,232 +545,6 @@ public class AuthorizeHttpRequestsConfigurerTests { this.mvc.perform(requestWithUser).andExpect(status().isForbidden()); } - @Test - public void configureWhenNoDispatcherServletThenSucceeds() throws Exception { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", Servlet.class).addMapping("/"); - this.spring.register(AuthorizeHttpRequestsConfig.class) - .postProcessor((context) -> context.setServletContext(servletContext)) - .autowire(); - this.mvc.perform(get("/path")).andExpect(status().isNotFound()); - } - - @Test - public void configureWhenOnlyDispatcherServletThenSucceeds() throws Exception { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - this.spring.register(AuthorizeHttpRequestsConfig.class) - .postProcessor((context) -> context.setServletContext(servletContext)) - .autowire(); - this.mvc.perform(get("/mvc/path").servletPath("/mvc")).andExpect(status().isNotFound()); - this.mvc.perform(get("/mvc")).andExpect(status().isUnauthorized()); - } - - @Test - public void configureWhenMultipleServletsThenSucceeds() throws Exception { - MockServletContext servletContext = MockServletContext.mvc(); - servletContext.addServlet("path", Servlet.class).addMapping("/path/*"); - this.spring.register(AuthorizeHttpRequestsConfig.class) - .postProcessor((context) -> context.setServletContext(servletContext)) - .autowire(); - this.mvc.perform(get("/path").with(servletPath("/path"))).andExpect(status().isNotFound()); - } - - @Test - public void configureWhenAmbiguousServletsThenWiringException() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - servletContext.addServlet("path", Servlet.class).addMapping("/path/*"); - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> this.spring.register(AuthorizeHttpRequestsConfig.class) - .postProcessor((context) -> context.setServletContext(servletContext)) - .autowire()); - } - - @Test - void defaultServletMatchersWhenDefaultServletThenPermits() throws Exception { - this.spring.register(DefaultServletConfig.class) - .postProcessor((context) -> context.setServletContext(MockServletContext.mvc())) - .autowire(); - this.mvc.perform(get("/path/path").with(defaultServlet())).andExpect(status().isNotFound()); - this.mvc.perform(get("/path/path").with(servletPath("/path"))).andExpect(status().isUnauthorized()); - } - - @Test - void defaultServletHttpMethodMatchersWhenDefaultServletThenPermits() throws Exception { - this.spring.register(DefaultServletConfig.class) - .postProcessor((context) -> context.setServletContext(MockServletContext.mvc())) - .autowire(); - this.mvc.perform(get("/path/method").with(defaultServlet())).andExpect(status().isNotFound()); - this.mvc.perform(head("/path/method").with(defaultServlet())).andExpect(status().isUnauthorized()); - this.mvc.perform(get("/path/method").with(servletPath("/path"))).andExpect(status().isUnauthorized()); - } - - @Test - void defaultServletWhenNoDefaultServletThenWiringException() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> this.spring.register(DefaultServletConfig.class) - .postProcessor((context) -> context.setServletContext(new MockServletContext())) - .autowire()); - } - - @Test - void servletPathMatchersWhenMatchingServletThenPermits() throws Exception { - MockServletContext servletContext = MockServletContext.mvc(); - servletContext.addServlet("path", Servlet.class).addMapping("/path/*"); - this.spring.register(ServletPathConfig.class) - .postProcessor((context) -> context.setServletContext(servletContext)) - .autowire(); - this.mvc.perform(get("/path/path").with(servletPath("/path"))).andExpect(status().isNotFound()); - this.mvc.perform(get("/path/path").with(defaultServlet())).andExpect(status().isUnauthorized()); - } - - @Test - void servletPathHttpMethodMatchersWhenMatchingServletThenPermits() throws Exception { - MockServletContext servletContext = MockServletContext.mvc(); - servletContext.addServlet("path", Servlet.class).addMapping("/path/*"); - this.spring.register(ServletPathConfig.class) - .postProcessor((context) -> context.setServletContext(servletContext)) - .autowire(); - this.mvc.perform(get("/path/method").with(servletPath("/path"))).andExpect(status().isNotFound()); - this.mvc.perform(head("/path/method").with(servletPath("/path"))).andExpect(status().isUnauthorized()); - this.mvc.perform(get("/path/method").with(defaultServlet())).andExpect(status().isUnauthorized()); - } - - @Test - void servletPathWhenNoMatchingPathThenWiringException() { - MockServletContext servletContext = MockServletContext.mvc(); - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> this.spring.register(ServletPathConfig.class) - .postProcessor((context) -> context.setServletContext(servletContext)) - .autowire()); - } - - @Test - void servletMappingMatchersWhenMatchingServletThenPermits() throws Exception { - MockServletContext servletContext = MockServletContext.mvc(); - servletContext.addServlet("jsp", Servlet.class).addMapping("*.jsp"); - this.spring.register(ServletMappingConfig.class) - .postProcessor((context) -> context.setServletContext(servletContext)) - .autowire(); - this.mvc.perform(get("/path/file.jsp").with(servletExtension(".jsp"))).andExpect(status().isNotFound()); - this.mvc.perform(get("/path/file.jsp").with(defaultServlet())).andExpect(status().isUnauthorized()); - } - - @Test - void servletMappingHttpMethodMatchersWhenMatchingServletThenPermits() throws Exception { - MockServletContext servletContext = MockServletContext.mvc(); - servletContext.addServlet("jsp", Servlet.class).addMapping("*.jsp"); - this.spring.register(ServletMappingConfig.class) - .postProcessor((context) -> context.setServletContext(servletContext)) - .autowire(); - this.mvc.perform(get("/method/file.jsp").with(servletExtension(".jsp"))).andExpect(status().isNotFound()); - this.mvc.perform(head("/method/file.jsp").with(servletExtension(".jsp"))).andExpect(status().isUnauthorized()); - this.mvc.perform(get("/method/file.jsp").with(defaultServlet())).andExpect(status().isUnauthorized()); - } - - @Test - void servletMappingWhenNoMatchingExtensionThenWiringException() { - MockServletContext servletContext = MockServletContext.mvc(); - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> this.spring.register(ServletMappingConfig.class) - .postProcessor((context) -> context.setServletContext(servletContext)) - .autowire()); - } - - @Test - void anyRequestWhenUsedWithDefaultServletThenDoesNotWire() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> this.spring.register(MixedServletEndpointConfig.class).autowire()) - .withMessageContaining("forServletPattern"); - } - - @Test - void servletWhenNoMatchingPathThenDenies() throws Exception { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", Servlet.class).addMapping("/"); - servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp"); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - this.spring.register(DefaultServletAndServletPathConfig.class) - .postProcessor((context) -> context.setServletContext(servletContext)) - .autowire(); - this.mvc.perform(get("/js/color.js").with(servletPath("/js"))).andExpect(status().isUnauthorized()); - this.mvc.perform(get("/mvc/controller").with(defaultServlet())).andExpect(status().isUnauthorized()); - this.mvc.perform(get("/js/color.js").with(defaultServlet())).andExpect(status().isNotFound()); - this.mvc.perform(get("/mvc/controller").with(servletPath("/mvc"))).andExpect(status().isUnauthorized()); - this.mvc.perform(get("/mvc/controller").with(user("user")).with(servletPath("/mvc"))) - .andExpect(status().isNotFound()); - } - - @Test - void permitAllWhenDefaultServletThenDoesNotWire() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> this.spring.register(MixedServletPermitAllConfig.class).autowire()) - .withMessageContaining("forServletPattern"); - } - - static RequestPostProcessor defaultServlet() { - return (request) -> { - String uri = request.getRequestURI(); - request.setHttpServletMapping(TestMockHttpServletMappings.defaultMapping()); - request.setServletPath(uri); - request.setPathInfo(""); - return request; - }; - } - - static RequestPostProcessor servletPath(String path) { - return (request) -> { - String uri = request.getRequestURI(); - request.setHttpServletMapping(TestMockHttpServletMappings.path(request, path)); - request.setServletPath(path); - request.setPathInfo(uri.substring(path.length())); - return request; - }; - } - - static RequestPostProcessor servletExtension(String extension) { - return (request) -> { - String uri = request.getRequestURI(); - request.setHttpServletMapping(TestMockHttpServletMappings.extension(request, extension)); - request.setServletPath(uri); - request.setPathInfo(""); - return request; - }; - } - - @Configuration - @EnableWebSecurity - static class GrantedAuthorityDefaultHasRoleConfig { - - @Bean - GrantedAuthorityDefaults grantedAuthorityDefaults() { - return new GrantedAuthorityDefaults("CUSTOM_PREFIX_"); - } - - @Bean - SecurityFilterChain myFilterChain(HttpSecurity http) throws Exception { - return http.authorizeHttpRequests((c) -> c.anyRequest().hasRole("USER")).build(); - } - - } - - @Configuration - @EnableWebSecurity - static class GrantedAuthorityDefaultHasAnyRoleConfig { - - @Bean - GrantedAuthorityDefaults grantedAuthorityDefaults() { - return new GrantedAuthorityDefaults("CUSTOM_PREFIX_"); - } - - @Bean - SecurityFilterChain myFilterChain(HttpSecurity http) throws Exception { - return http.authorizeHttpRequests((c) -> c.anyRequest().hasAnyRole("USER", "ADMIN")).build(); - } - - } - @Configuration @EnableWebSecurity static class NoRequestsConfig { @@ -893,7 +610,6 @@ public class AuthorizeHttpRequestsConfigurerTests { @Configuration @EnableWebSecurity - @EnableWebMvc static class AfterAnyRequestConfig { @Bean @@ -1055,30 +771,6 @@ public class AuthorizeHttpRequestsConfigurerTests { } - @Configuration - @EnableWebSecurity - static class RoleHierarchyUserConfig { - - @Bean - SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - // @formatter:off - return http - .authorizeHttpRequests((requests) -> requests - .anyRequest().hasRole("USER") - ) - .build(); - // @formatter:on - } - - @Bean - RoleHierarchy roleHierarchy() { - RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); - roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER"); - return roleHierarchy; - } - - } - @Configuration @EnableWebSecurity static class RoleUserOrAdminConfig { @@ -1155,7 +847,7 @@ public class AuthorizeHttpRequestsConfigurerTests { @Configuration @EnableWebMvc @EnableWebSecurity - static class MvcServletPathConfig { + static class ServletPathConfig { @Bean SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { @@ -1337,163 +1029,6 @@ public class AuthorizeHttpRequestsConfigurerTests { } - @Configuration - @EnableWebSecurity - @EnableWebMvc - static class AuthorizeHttpRequestsConfig { - - @Bean - SecurityFilterChain chain(HttpSecurity http) throws Exception { - // @formatter:off - http - .httpBasic(withDefaults()) - .authorizeHttpRequests((requests) -> requests - .requestMatchers("/path/**").permitAll() - .anyRequest().authenticated() - ); - // @formatter:on - return http.build(); - } - - } - - @Configuration - @EnableWebSecurity - @EnableWebMvc - static class DefaultServletConfig { - - @Bean - SecurityFilterChain chain(HttpSecurity http) throws Exception { - // @formatter:off - http - .httpBasic(withDefaults()) - .authorizeHttpRequests((requests) -> requests - .forServletPattern("/", (root) -> root - .requestMatchers(HttpMethod.GET, "/path/method/**").permitAll() - .requestMatchers("/path/path/**").permitAll() - .anyRequest().authenticated() - ) - ); - // @formatter:on - return http.build(); - } - - } - - @Configuration - @EnableWebSecurity - @EnableWebMvc - static class ServletPathConfig { - - @Bean - SecurityFilterChain chain(HttpSecurity http) throws Exception { - // @formatter:off - http - .httpBasic(withDefaults()) - .authorizeHttpRequests((requests) -> requests - .forServletPattern("/path/*", (root) -> root - .requestMatchers(HttpMethod.GET, "/method/**").permitAll() - .requestMatchers("/path/**").permitAll() - .anyRequest().authenticated() - ) - ); - // @formatter:on - return http.build(); - } - - } - - @Configuration - @EnableWebSecurity - @EnableWebMvc - static class ServletMappingConfig { - - @Bean - SecurityFilterChain chain(HttpSecurity http) throws Exception { - // @formatter:off - http - .httpBasic(withDefaults()) - .authorizeHttpRequests((requests) -> requests - .forServletPattern("*.jsp", (jsp) -> jsp - .requestMatchers(HttpMethod.GET, "/method/**").permitAll() - .requestMatchers("/path/**").permitAll() - .anyRequest().authenticated() - ) - ); - // @formatter:on - return http.build(); - } - - } - - @Configuration - @EnableWebSecurity - @EnableWebMvc - static class MixedServletEndpointConfig { - - @Bean - SecurityFilterChain chain(HttpSecurity http) throws Exception { - // @formatter:off - http - .httpBasic(withDefaults()) - .authorizeHttpRequests((requests) -> requests - .forServletPattern("/", (root) -> root.anyRequest().permitAll()) - .anyRequest().authenticated() - ); - // @formatter:on - return http.build(); - } - - } - - @Configuration - @EnableWebSecurity - @EnableWebMvc - static class MixedServletPermitAllConfig { - - @Bean - SecurityFilterChain chain(HttpSecurity http) throws Exception { - // @formatter:off - http - .formLogin((form) -> form.loginPage("/page").permitAll()) - .authorizeHttpRequests((requests) -> requests - .forServletPattern("/", (root) -> root - .anyRequest().authenticated() - ) - ); - // @formatter:on - return http.build(); - } - - } - - @Configuration - @EnableWebSecurity - @EnableWebMvc - static class DefaultServletAndServletPathConfig { - - @Bean - SecurityFilterChain chain(HttpSecurity http) throws Exception { - // @formatter:off - http - .httpBasic(withDefaults()) - .authorizeHttpRequests((requests) -> requests - .forServletPattern("/", (root) -> root - .requestMatchers("/js/**", "/css/**").permitAll() - ) - .forServletPattern("/mvc/*", (mvc) -> mvc - .requestMatchers("/controller/**").authenticated() - ) - .forServletPattern("*.jsp", (jsp) -> jsp - .anyRequest().authenticated() - ) - ); - // @formatter:on - return http.build(); - } - - } - @Configuration static class AuthorizationEventPublisherConfig { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.java index d551a2e305..6bf4158278 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.java @@ -85,10 +85,10 @@ public class DefaultFiltersTests { List filterChains = this.spring.getContext() .getBean(FilterChainProxy.class) .getFilterChains(); - assertThat(filterChains).hasSize(1); + assertThat(filterChains.size()).isEqualTo(1); DefaultSecurityFilterChain filterChain = (DefaultSecurityFilterChain) filterChains.get(0); assertThat(filterChain.getRequestMatcher()).isInstanceOf(AnyRequestMatcher.class); - assertThat(filterChain.getFilters()).hasSize(1); + assertThat(filterChain.getFilters().size()).isEqualTo(1); long filter = filterChain.getFilters() .stream() .filter((it) -> it instanceof UsernamePasswordAuthenticationFilter) @@ -102,25 +102,25 @@ public class DefaultFiltersTests { List filterChains = this.spring.getContext() .getBean(FilterChainProxy.class) .getFilterChains(); - assertThat(filterChains).hasSize(2); + assertThat(filterChains.size()).isEqualTo(2); DefaultSecurityFilterChain firstFilter = (DefaultSecurityFilterChain) filterChains.get(0); DefaultSecurityFilterChain secondFilter = (DefaultSecurityFilterChain) filterChains.get(1); assertThat(firstFilter.getFilters().isEmpty()).isEqualTo(true); assertThat(secondFilter.getRequestMatcher()).isInstanceOf(AnyRequestMatcher.class); - List> classes = secondFilter.getFilters() + List> classes = secondFilter.getFilters() .stream() .map(Filter::getClass) .collect(Collectors.toList()); - assertThat(classes).contains(WebAsyncManagerIntegrationFilter.class); - assertThat(classes).contains(SecurityContextHolderFilter.class); - assertThat(classes).contains(HeaderWriterFilter.class); - assertThat(classes).contains(LogoutFilter.class); - assertThat(classes).contains(CsrfFilter.class); - assertThat(classes).contains(RequestCacheAwareFilter.class); - assertThat(classes).contains(SecurityContextHolderAwareRequestFilter.class); - assertThat(classes).contains(AnonymousAuthenticationFilter.class); - assertThat(classes).contains(ExceptionTranslationFilter.class); - assertThat(classes).contains(FilterSecurityInterceptor.class); + assertThat(classes.contains(WebAsyncManagerIntegrationFilter.class)).isTrue(); + assertThat(classes.contains(SecurityContextHolderFilter.class)).isTrue(); + assertThat(classes.contains(HeaderWriterFilter.class)).isTrue(); + assertThat(classes.contains(LogoutFilter.class)).isTrue(); + assertThat(classes.contains(CsrfFilter.class)).isTrue(); + assertThat(classes.contains(RequestCacheAwareFilter.class)).isTrue(); + assertThat(classes.contains(SecurityContextHolderAwareRequestFilter.class)).isTrue(); + assertThat(classes.contains(AnonymousAuthenticationFilter.class)).isTrue(); + assertThat(classes.contains(ExceptionTranslationFilter.class)).isTrue(); + assertThat(classes.contains(FilterSecurityInterceptor.class)).isTrue(); } @Test diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java index 3b44f86ed8..4fbc0bc045 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -96,7 +96,7 @@ public class DefaultLoginPageConfigurerTests { + " \n" + " Please sign in\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
    \n" @@ -145,7 +145,7 @@ public class DefaultLoginPageConfigurerTests { + " \n" + " Please sign in\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
    \n" @@ -197,7 +197,7 @@ public class DefaultLoginPageConfigurerTests { + " \n" + " Please sign in\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
    \n" @@ -250,7 +250,7 @@ public class DefaultLoginPageConfigurerTests { + " \n" + " Please sign in\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
    \n" diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerEagerHeadersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerEagerHeadersTests.java index 31468c5711..c805f16aa3 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerEagerHeadersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerEagerHeadersTests.java @@ -31,8 +31,6 @@ import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -52,7 +50,7 @@ public class HeadersConfigurerEagerHeadersTests { @Test public void requestWhenHeadersEagerlyConfiguredThenHeadersAreWritten() throws Exception { - this.spring.register(HeadersAtTheBeginningOfRequestConfig.class, HomeController.class).autowire(); + this.spring.register(HeadersAtTheBeginningOfRequestConfig.class).autowire(); this.mvc.perform(get("/").secure(true)) .andExpect(header().string("X-Content-Type-Options", "nosniff")) .andExpect(header().string("X-Frame-Options", "DENY")) @@ -85,14 +83,4 @@ public class HeadersConfigurerEagerHeadersTests { } - @RestController - private static class HomeController { - - @GetMapping("/") - String ok() { - return "ok"; - } - - } - } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java index 25cfac15e6..13ee613729 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,6 @@ import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextChangedListener; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @@ -43,7 +42,6 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.web.bind.annotation.GetMapping; @@ -68,7 +66,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * * @author Rob Winch * @author Eleftheria Stein - * @author Evgeniy Cheban */ @ExtendWith(SpringTestContextExtension.class) public class HttpBasicConfigurerTests { @@ -107,7 +104,6 @@ public class HttpBasicConfigurerTests { @Test public void httpBasicWhenUsingCustomAuthenticationEntryPointThenResponseIncludesBasicChallenge() throws Exception { - CustomAuthenticationEntryPointConfig.ENTRY_POINT = mock(AuthenticationEntryPoint.class); this.spring.register(CustomAuthenticationEntryPointConfig.class).autowire(); this.mvc.perform(get("/")); verify(CustomAuthenticationEntryPointConfig.ENTRY_POINT).commence(any(HttpServletRequest.class), @@ -125,7 +121,7 @@ public class HttpBasicConfigurerTests { // SEC-3019 @Test public void httpBasicWhenRememberMeConfiguredThenSetsRememberMeCookie() throws Exception { - this.spring.register(BasicUsesRememberMeConfig.class, Home.class).autowire(); + this.spring.register(BasicUsesRememberMeConfig.class).autowire(); MockHttpServletRequestBuilder rememberMeRequest = get("/").with(httpBasic("user", "password")) .param("remember-me", "true"); this.mvc.perform(rememberMeRequest).andExpect(cookie().exists("remember-me")); @@ -151,16 +147,6 @@ public class HttpBasicConfigurerTests { verify(listener).securityContextChanged(setAuthentication(UsernamePasswordAuthenticationToken.class)); } - @Test - public void httpBasicWhenUsingCustomSecurityContextRepositoryThenUses() throws Exception { - this.spring.register(CustomSecurityContextRepositoryConfig.class, Users.class, Home.class).autowire(); - this.mvc.perform(get("/").with(httpBasic("user", "password"))) - .andExpect(status().isOk()) - .andExpect(content().string("user")); - verify(CustomSecurityContextRepositoryConfig.SECURITY_CONTEXT_REPOSITORY) - .saveContext(any(SecurityContext.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - } - @Configuration @EnableWebSecurity static class ObjectPostProcessorConfig { @@ -243,7 +229,7 @@ public class HttpBasicConfigurerTests { @EnableWebSecurity static class CustomAuthenticationEntryPointConfig { - static AuthenticationEntryPoint ENTRY_POINT; + static AuthenticationEntryPoint ENTRY_POINT = mock(AuthenticationEntryPoint.class); @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -337,24 +323,6 @@ public class HttpBasicConfigurerTests { } - @Configuration - @EnableWebSecurity - static class CustomSecurityContextRepositoryConfig { - - static final SecurityContextRepository SECURITY_CONTEXT_REPOSITORY = mock(SecurityContextRepository.class); - - @Bean - SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - // @formatter:off - http - .httpBasic() - .securityContextRepository(SECURITY_CONTEXT_REPOSITORY); - // @formatter:on - return http.build(); - } - - } - @Configuration static class Users { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java index 1b14a87b33..5bde6992a6 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java @@ -382,7 +382,7 @@ public class RequestCacheConfigurerTests { .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()) - .requestCache(RequestCacheConfigurer::disable); + .requestCache((cache) -> cache.disable()); // @formatter:on return http.build(); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuildersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuildersTests.java deleted file mode 100644 index 61fdf6b2be..0000000000 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuildersTests.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers; - -import java.util.List; -import java.util.function.Consumer; - -import jakarta.servlet.Servlet; -import jakarta.servlet.ServletContext; -import org.junit.jupiter.api.Test; - -import org.springframework.http.HttpMethod; -import org.springframework.security.config.MockServletContext; -import org.springframework.security.config.annotation.ObjectPostProcessor; -import org.springframework.security.config.annotation.web.configurers.DispatcherServletDelegatingRequestMatcherBuilder.DispatcherServletDelegatingRequestMatcher; -import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.context.support.GenericWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.mock; - -public class RequestMatcherBuildersTests { - - @Test - void matchersWhenDefaultDispatcherServletThenMvc() { - MockServletContext servletContext = MockServletContext.mvc(); - RequestMatcherBuilder builder = requestMatchersBuilder(servletContext); - List matchers = builder.matchers("/mvc"); - assertThat(matchers.get(0)).isInstanceOf(MvcRequestMatcher.class); - MvcRequestMatcher matcher = (MvcRequestMatcher) matchers.get(0); - assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isNull(); - assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/mvc"); - } - - @Test - void httpMethodMatchersWhenDefaultDispatcherServletThenMvc() { - MockServletContext servletContext = MockServletContext.mvc(); - RequestMatcherBuilder builder = requestMatchersBuilder(servletContext); - List matchers = builder.matchers(HttpMethod.GET, "/mvc"); - assertThat(matchers.get(0)).isInstanceOf(MvcRequestMatcher.class); - MvcRequestMatcher matcher = (MvcRequestMatcher) matchers.get(0); - assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isNull(); - assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/mvc"); - assertThat(ReflectionTestUtils.getField(matcher, "method")).isEqualTo(HttpMethod.GET); - } - - @Test - void matchersWhenPathDispatcherServletThenMvc() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - RequestMatcherBuilder builder = requestMatchersBuilder(servletContext); - List matchers = builder.matchers("/controller"); - assertThat(matchers.get(0)).isInstanceOf(MvcRequestMatcher.class); - MvcRequestMatcher matcher = (MvcRequestMatcher) matchers.get(0); - assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isEqualTo("/mvc"); - assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller"); - } - - @Test - void matchersWhenAlsoExtraServletContainerMappingsThenRequiresServletPath() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", Servlet.class).addMapping("/"); - servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx"); - servletContext.addServlet("facesServlet", Servlet.class).addMapping("/faces/", "*.jsf", "*.faces", "*.xhtml"); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path")) - .withMessageContaining(".forServletPattern"); - } - - @Test - void matchersWhenOnlyDefaultServletThenAnt() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", Servlet.class).addMapping("/"); - RequestMatcherBuilder builder = requestMatchersBuilder(servletContext); - List matchers = builder.matchers("/controller"); - assertThat(matchers.get(0)).isInstanceOf(AntPathRequestMatcher.class); - AntPathRequestMatcher matcher = (AntPathRequestMatcher) matchers.get(0); - assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller"); - } - - @Test - void matchersWhenNoHandlerMappingIntrospectorThenAnt() { - MockServletContext servletContext = MockServletContext.mvc(); - RequestMatcherBuilder builder = requestMatchersBuilder(servletContext, (context) -> { - }); - List matchers = builder.matchers("/controller"); - assertThat(matchers.get(0)).isInstanceOf(AntPathRequestMatcher.class); - AntPathRequestMatcher matcher = (AntPathRequestMatcher) matchers.get(0); - assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller"); - } - - @Test - void matchersWhenNoDispatchServletThenAnt() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", Servlet.class).addMapping("/"); - servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*"); - RequestMatcherBuilder builder = requestMatchersBuilder(servletContext); - List matchers = builder.matchers("/services/endpoint"); - assertThat(matchers.get(0)).isInstanceOf(AntPathRequestMatcher.class); - AntPathRequestMatcher matcher = (AntPathRequestMatcher) matchers.get(0); - assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/services/endpoint"); - } - - @Test - void matchersWhenMixedServletsThenServletPathDelegating() { - MockServletContext servletContext = MockServletContext.mvc(); - servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*"); - RequestMatcherBuilder builder = requestMatchersBuilder(servletContext); - assertThat(builder.matchers("/services/endpoint").get(0)) - .isInstanceOf(DispatcherServletDelegatingRequestMatcher.class); - } - - @Test - void matchersWhenDispatcherServletNotDefaultAndOtherServletsThenRequiresServletPath() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", Servlet.class).addMapping("/"); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path/**")) - .withMessageContaining(".forServletPattern"); - } - - @Test - void httpMatchersWhenDispatcherServletNotDefaultAndOtherServletsThenRequiresServletPath() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("default", Servlet.class).addMapping("/"); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*"); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/pattern")) - .withMessageContaining(".forServletPattern"); - } - - @Test - void matchersWhenTwoDispatcherServletsThenException() { - MockServletContext servletContext = MockServletContext.mvc(); - servletContext.addServlet("two", DispatcherServlet.class).addMapping("/other/*"); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path/**")) - .withMessageContaining(".forServletPattern"); - } - - @Test - void matchersWhenMoreThanOneMappingThenException() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/two/*"); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path/**")) - .withMessageContaining(".forServletPattern"); - } - - @Test - void matchersWhenMoreThanOneMappingAndDefaultServletsThenRequiresServletPath() { - MockServletContext servletContext = new MockServletContext(); - servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/two/*"); - servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx"); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path/**")) - .withMessageContaining(".forServletPattern"); - } - - RequestMatcherBuilder requestMatchersBuilder(ServletContext servletContext) { - return requestMatchersBuilder(servletContext, (context) -> { - context.registerBean("mvcHandlerMappingIntrospector", HandlerMappingIntrospector.class, - () -> mock(HandlerMappingIntrospector.class)); - context.registerBean(ObjectPostProcessor.class, () -> mock(ObjectPostProcessor.class)); - }); - } - - RequestMatcherBuilder requestMatchersBuilder(ServletContext servletContext, - Consumer consumer) { - GenericWebApplicationContext context = new GenericWebApplicationContext(servletContext); - consumer.accept(context); - context.refresh(); - return RequestMatcherBuilders.createDefault(context); - } - -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletPatternRequestMatcherTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletPatternRequestMatcherTests.java deleted file mode 100644 index 98a371bbc7..0000000000 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletPatternRequestMatcherTests.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers; - -import org.junit.jupiter.api.Test; - -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.security.config.TestMockHttpServletMappings; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ServletPatternRequestMatcher} - */ -class ServletPatternRequestMatcherTests { - - ServletPatternRequestMatcher matcher = new ServletPatternRequestMatcher("*.jsp"); - - @Test - void matchesWhenDefaultServletThenTrue() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a/uri.jsp"); - request.setHttpServletMapping(TestMockHttpServletMappings.extension(request, ".jsp")); - assertThat(this.matcher.matches(request)).isTrue(); - } - - @Test - void matchesWhenNotDefaultServletThenFalse() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a/uri.jsp"); - request.setHttpServletMapping(TestMockHttpServletMappings.path(request, "/a")); - request.setServletPath("/a/uri.jsp"); - assertThat(this.matcher.matches(request)).isFalse(); - } - - @Test - void matcherWhenDefaultServletThenTrue() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a/uri.jsp"); - request.setHttpServletMapping(TestMockHttpServletMappings.extension(request, ".jsp")); - request.setServletPath("/a/uri.jsp"); - assertThat(this.matcher.matcher(request).isMatch()).isTrue(); - } - - @Test - void matcherWhenNotDefaultServletThenFalse() { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a/uri.jsp"); - request.setHttpServletMapping(TestMockHttpServletMappings.path(request, "/a")); - request.setServletPath("/a/uri.jsp"); - assertThat(this.matcher.matcher(request).isMatch()).isFalse(); - } - -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java index 168564a8ee..7ebd37c74b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java @@ -556,7 +556,9 @@ public class SessionManagementConfigurerTests { .sessionManagement((sessionManagement) -> sessionManagement .requireExplicitAuthenticationStrategy(false) - .sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::newSession) + .sessionFixation((sessionFixation) -> + sessionFixation.newSession() + ) ) .httpBasic(withDefaults()); // @formatter:on diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java deleted file mode 100644 index cecc69feee..0000000000 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java +++ /dev/null @@ -1,596 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.oauth2.client; - -import java.io.IOException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.interfaces.RSAPublicKey; -import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import com.gargoylesoftware.htmlunit.util.UrlUtils; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.source.ImmutableJWKSet; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.oauth2.sdk.Scope; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; -import com.nimbusds.openid.connect.sdk.token.OIDCTokens; -import jakarta.annotation.PreDestroy; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; -import okhttp3.mockwebserver.Dispatcher; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.core.annotation.Order; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.mock.web.MockHttpSession; -import org.springframework.mock.web.MockServletContext; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.test.SpringTestContext; -import org.springframework.security.config.test.SpringTestContextExtension; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimNames; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens; -import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.TestClientRegistrations; -import org.springframework.security.oauth2.core.oidc.OidcIdToken; -import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.oauth2.jwt.JwtClaimsSet; -import org.springframework.security.oauth2.jwt.JwtEncoder; -import org.springframework.security.oauth2.jwt.JwtEncoderParameters; -import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; -import org.springframework.security.provisioning.InMemoryUserDetailsManager; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -import static org.hamcrest.Matchers.containsString; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Tests for {@link OidcLogoutConfigurer} - */ -@ExtendWith(SpringTestContextExtension.class) -public class OidcLogoutConfigurerTests { - - @Autowired - private MockMvc mvc; - - @Autowired(required = false) - private MockWebServer web; - - @Autowired - private ClientRegistration clientRegistration; - - public final SpringTestContext spring = new SpringTestContext(this); - - @Test - void logoutWhenDefaultsThenRemotelyInvalidatesSessions() throws Exception { - this.spring.register(WebServerConfig.class, OidcProviderConfig.class, DefaultConfig.class).autowire(); - String registrationId = this.clientRegistration.getRegistrationId(); - MockHttpSession session = login(); - String logoutToken = this.mvc.perform(get("/token/logout").session(session)) - .andExpect(status().isOk()) - .andReturn() - .getResponse() - .getContentAsString(); - this.mvc - .perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) - .param("logout_token", logoutToken)) - .andExpect(status().isOk()); - this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isUnauthorized()); - } - - @Test - void logoutWhenInvalidLogoutTokenThenBadRequest() throws Exception { - this.spring.register(WebServerConfig.class, OidcProviderConfig.class, DefaultConfig.class).autowire(); - this.mvc.perform(get("/token/logout")).andExpect(status().isUnauthorized()); - String registrationId = this.clientRegistration.getRegistrationId(); - MvcResult result = this.mvc.perform(get("/oauth2/authorization/" + registrationId)) - .andExpect(status().isFound()) - .andReturn(); - MockHttpSession session = (MockHttpSession) result.getRequest().getSession(); - String redirectUrl = UrlUtils.decode(result.getResponse().getRedirectedUrl()); - String state = this.mvc - .perform(get(redirectUrl) - .with(httpBasic(this.clientRegistration.getClientId(), this.clientRegistration.getClientSecret()))) - .andReturn() - .getResponse() - .getContentAsString(); - result = this.mvc - .perform(get("/login/oauth2/code/" + registrationId).param("code", "code") - .param("state", state) - .session(session)) - .andExpect(status().isFound()) - .andReturn(); - session = (MockHttpSession) result.getRequest().getSession(); - this.mvc - .perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) - .param("logout_token", "invalid")) - .andExpect(status().isBadRequest()); - this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isOk()); - } - - @Test - void logoutWhenLogoutTokenSpecifiesOneSessionThenRemotelyInvalidatesOnlyThatSession() throws Exception { - this.spring.register(WebServerConfig.class, OidcProviderConfig.class, DefaultConfig.class).autowire(); - String registrationId = this.clientRegistration.getRegistrationId(); - MockHttpSession one = login(); - MockHttpSession two = login(); - MockHttpSession three = login(); - String logoutToken = this.mvc.perform(get("/token/logout").session(one)) - .andExpect(status().isOk()) - .andReturn() - .getResponse() - .getContentAsString(); - this.mvc - .perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) - .param("logout_token", logoutToken)) - .andExpect(status().isOk()); - this.mvc.perform(get("/token/logout").session(one)).andExpect(status().isUnauthorized()); - this.mvc.perform(get("/token/logout").session(two)).andExpect(status().isOk()); - logoutToken = this.mvc.perform(get("/token/logout/all").session(three)) - .andExpect(status().isOk()) - .andReturn() - .getResponse() - .getContentAsString(); - this.mvc - .perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) - .param("logout_token", logoutToken)) - .andExpect(status().isOk()); - this.mvc.perform(get("/token/logout").session(two)).andExpect(status().isUnauthorized()); - this.mvc.perform(get("/token/logout").session(three)).andExpect(status().isUnauthorized()); - } - - @Test - void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() throws Exception { - this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire(); - LogoutHandler logoutHandler = this.spring.getContext().getBean(LogoutHandler.class); - willThrow(IllegalStateException.class).given(logoutHandler).logout(any(), any(), any()); - String registrationId = this.clientRegistration.getRegistrationId(); - MockHttpSession one = login(); - String logoutToken = this.mvc.perform(get("/token/logout/all").session(one)) - .andExpect(status().isOk()) - .andReturn() - .getResponse() - .getContentAsString(); - this.mvc - .perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) - .param("logout_token", logoutToken)) - .andExpect(status().isBadRequest()) - .andExpect(content().string(containsString("partial_logout"))); - this.mvc.perform(get("/token/logout").session(one)).andExpect(status().isOk()); - } - - @Test - void logoutWhenCustomComponentsThenUses() throws Exception { - this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithCustomComponentsConfig.class) - .autowire(); - String registrationId = this.clientRegistration.getRegistrationId(); - MockHttpSession session = login(); - String logoutToken = this.mvc.perform(get("/token/logout").session(session)) - .andExpect(status().isOk()) - .andReturn() - .getResponse() - .getContentAsString(); - this.mvc - .perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) - .param("logout_token", logoutToken)) - .andExpect(status().isOk()); - this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isUnauthorized()); - OidcSessionRegistry sessionRegistry = this.spring.getContext().getBean(OidcSessionRegistry.class); - verify(sessionRegistry).saveSessionInformation(any()); - verify(sessionRegistry).removeSessionInformation(any(OidcLogoutToken.class)); - } - - private MockHttpSession login() throws Exception { - MockMvcDispatcher dispatcher = (MockMvcDispatcher) this.web.getDispatcher(); - this.mvc.perform(get("/token/logout")).andExpect(status().isUnauthorized()); - String registrationId = this.clientRegistration.getRegistrationId(); - MvcResult result = this.mvc.perform(get("/oauth2/authorization/" + registrationId)) - .andExpect(status().isFound()) - .andReturn(); - MockHttpSession session = (MockHttpSession) result.getRequest().getSession(); - String redirectUrl = UrlUtils.decode(result.getResponse().getRedirectedUrl()); - String state = this.mvc - .perform(get(redirectUrl) - .with(httpBasic(this.clientRegistration.getClientId(), this.clientRegistration.getClientSecret()))) - .andReturn() - .getResponse() - .getContentAsString(); - result = this.mvc - .perform(get("/login/oauth2/code/" + registrationId).param("code", "code") - .param("state", state) - .session(session)) - .andExpect(status().isFound()) - .andReturn(); - session = (MockHttpSession) result.getRequest().getSession(); - dispatcher.registerSession(session); - return session; - } - - @Configuration - static class RegistrationConfig { - - @Autowired(required = false) - MockWebServer web; - - @Bean - ClientRegistration clientRegistration() { - if (this.web == null) { - return TestClientRegistrations.clientRegistration().build(); - } - String issuer = this.web.url("/").toString(); - return TestClientRegistrations.clientRegistration() - .issuerUri(issuer) - .jwkSetUri(issuer + "jwks") - .tokenUri(issuer + "token") - .userInfoUri(issuer + "user") - .scope("openid") - .build(); - } - - @Bean - ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) { - return new InMemoryClientRegistrationRepository(clientRegistration); - } - - } - - @Configuration - @EnableWebSecurity - @Import(RegistrationConfig.class) - static class DefaultConfig { - - @Bean - @Order(1) - SecurityFilterChain filters(HttpSecurity http) throws Exception { - // @formatter:off - http - .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) - .oauth2Login(Customizer.withDefaults()) - .oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults())); - // @formatter:on - - return http.build(); - } - - } - - @Configuration - @EnableWebSecurity - @Import(RegistrationConfig.class) - static class WithCustomComponentsConfig { - - OidcSessionRegistry sessionRegistry = spy(new InMemoryOidcSessionRegistry()); - - @Bean - @Order(1) - SecurityFilterChain filters(HttpSecurity http) throws Exception { - // @formatter:off - http - .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) - .oauth2Login((oauth2) -> oauth2.oidcSessionRegistry(this.sessionRegistry)) - .oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults())); - // @formatter:on - - return http.build(); - } - - @Bean - OidcSessionRegistry sessionRegistry() { - return this.sessionRegistry; - } - - } - - @Configuration - @EnableWebSecurity - @Import(RegistrationConfig.class) - static class WithBrokenLogoutConfig { - - private final LogoutHandler logoutHandler = mock(LogoutHandler.class); - - @Bean - @Order(1) - SecurityFilterChain filters(HttpSecurity http) throws Exception { - // @formatter:off - http - .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) - .logout((logout) -> logout.addLogoutHandler(this.logoutHandler)) - .oauth2Login(Customizer.withDefaults()) - .oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults())); - // @formatter:on - - return http.build(); - } - - @Bean - LogoutHandler logoutHandler() { - return this.logoutHandler; - } - - } - - @Configuration - @EnableWebSecurity - @EnableWebMvc - @RestController - static class OidcProviderConfig { - - private static final RSAKey key = key(); - - private static final JWKSource jwks = jwks(key); - - private static RSAKey key() { - try { - KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); - return new RSAKey.Builder((RSAPublicKey) pair.getPublic()).privateKey(pair.getPrivate()).build(); - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - private static JWKSource jwks(RSAKey key) { - try { - return new ImmutableJWKSet<>(new JWKSet(key)); - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - private final String username = "user"; - - private final JwtEncoder encoder = new NimbusJwtEncoder(jwks); - - private String nonce; - - @Autowired - ClientRegistration registration; - - @Bean - @Order(0) - SecurityFilterChain authorizationServer(HttpSecurity http, ClientRegistration registration) throws Exception { - // @formatter:off - http - .securityMatcher("/jwks", "/login/oauth/authorize", "/nonce", "/token", "/token/logout", "/user") - .authorizeHttpRequests((authorize) -> authorize - .requestMatchers("/jwks").permitAll() - .anyRequest().authenticated() - ) - .httpBasic(Customizer.withDefaults()) - .oauth2ResourceServer((oauth2) -> oauth2 - .jwt((jwt) -> jwt.jwkSetUri(registration.getProviderDetails().getJwkSetUri())) - ); - // @formatter:off - - return http.build(); - } - - @Bean - UserDetailsService users(ClientRegistration registration) { - return new InMemoryUserDetailsManager(User.withUsername(registration.getClientId()) - .password("{noop}" + registration.getClientSecret()).authorities("APP").build()); - } - - @GetMapping("/login/oauth/authorize") - String nonce(@RequestParam("nonce") String nonce, @RequestParam("state") String state) { - this.nonce = nonce; - return state; - } - - @PostMapping("/token") - Map accessToken(HttpServletRequest request) { - HttpSession session = request.getSession(); - JwtEncoderParameters parameters = JwtEncoderParameters - .from(JwtClaimsSet.builder().id("id").subject(this.username) - .issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now()) - .expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build()); - String token = this.encoder.encode(parameters).getTokenValue(); - return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null) - .toJSONObject(); - } - - String idToken(String sessionId) { - OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri()) - .subject(this.username).expiresAt(Instant.now().plusSeconds(86400)) - .audience(List.of(this.registration.getClientId())).nonce(this.nonce) - .claim(LogoutTokenClaimNames.SID, sessionId).build(); - JwtEncoderParameters parameters = JwtEncoderParameters - .from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build()); - return this.encoder.encode(parameters).getTokenValue(); - } - - @GetMapping("/user") - Map userinfo() { - return Map.of("sub", this.username, "id", this.username); - } - - @GetMapping("/jwks") - String jwks() { - return new JWKSet(key).toString(); - } - - @GetMapping("/token/logout") - String logoutToken(@AuthenticationPrincipal OidcUser user) { - OidcLogoutToken token = TestOidcLogoutTokens.withUser(user) - .audience(List.of(this.registration.getClientId())).build(); - JwtEncoderParameters parameters = JwtEncoderParameters - .from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build()); - return this.encoder.encode(parameters).getTokenValue(); - } - - @GetMapping("/token/logout/all") - String logoutTokenAll(@AuthenticationPrincipal OidcUser user) { - OidcLogoutToken token = TestOidcLogoutTokens.withUser(user) - .audience(List.of(this.registration.getClientId())) - .claims((claims) -> claims.remove(LogoutTokenClaimNames.SID)).build(); - JwtEncoderParameters parameters = JwtEncoderParameters - .from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build()); - return this.encoder.encode(parameters).getTokenValue(); - } - } - - @Configuration - static class WebServerConfig { - - private final MockWebServer server = new MockWebServer(); - - @Bean - MockWebServer web(ObjectProvider mvc) { - this.server.setDispatcher(new MockMvcDispatcher(mvc)); - return this.server; - } - - @PreDestroy - void shutdown() throws IOException { - this.server.shutdown(); - } - - } - - private static class MockMvcDispatcher extends Dispatcher { - - private final Map session = new ConcurrentHashMap<>(); - - private final ObjectProvider mvcProvider; - - private MockMvc mvc; - - MockMvcDispatcher(ObjectProvider mvc) { - this.mvcProvider = mvc; - } - - @Override - public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - this.mvc = this.mvcProvider.getObject(); - String method = request.getMethod(); - String path = request.getPath(); - String csrf = request.getHeader("X-CSRF-TOKEN"); - MockHttpSession session = session(request); - MockHttpServletRequestBuilder builder; - if ("GET".equals(method)) { - builder = get(path); - } - else { - builder = post(path).content(request.getBody().readUtf8()); - if (csrf != null) { - builder.header("X-CSRF-TOKEN", csrf); - } - else { - builder.with(csrf()); - } - } - for (Map.Entry> header : request.getHeaders().toMultimap().entrySet()) { - builder.header(header.getKey(), header.getValue().iterator().next()); - } - try { - MockHttpServletResponse mvcResponse = this.mvc.perform(builder.session(session)).andReturn().getResponse(); - return toMockResponse(mvcResponse); - } - catch (Exception ex) { - MockResponse response = new MockResponse(); - response.setResponseCode(500); - return response; - } - } - - void registerSession(MockHttpSession session) { - this.session.put(session.getId(), session); - } - - private MockHttpSession session(RecordedRequest request) { - String cookieHeaderValue = request.getHeader("Cookie"); - if (cookieHeaderValue == null) { - return new MockHttpSession(); - } - String[] cookies = cookieHeaderValue.split(";"); - for (String cookie : cookies) { - String[] parts = cookie.split("="); - if ("JSESSIONID".equals(parts[0])) { - return this.session.computeIfAbsent(parts[1], - (k) -> new MockHttpSession(new MockServletContext(), parts[1])); - } - } - return new MockHttpSession(); - } - - private MockResponse toMockResponse(MockHttpServletResponse mvcResponse) { - MockResponse response = new MockResponse(); - response.setResponseCode(mvcResponse.getStatus()); - for (String name : mvcResponse.getHeaderNames()) { - response.addHeader(name, mvcResponse.getHeaderValue(name)); - } - response.setBody(getContentAsString(mvcResponse)); - return response; - } - - private String getContentAsString(MockHttpServletResponse response) { - try { - return response.getContentAsString(); - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - } - -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index cb2ba0e137..ca418a865c 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -873,7 +873,8 @@ public class OAuth2ResourceServerConfigurerTests { context.registerBean("decoderTwo", JwtDecoder.class, () -> decoder); this.spring.context(context).autowire(); OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt(); - assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(jwtConfigurer::getJwtDecoder); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(() -> jwtConfigurer.getJwtDecoder()); } @Test @@ -1896,7 +1897,9 @@ public class OAuth2ResourceServerConfigurerTests { .anyRequest().authenticated() ) .oauth2Login(withDefaults()) - .oauth2ResourceServer((oauth2) -> oauth2.jwt(withDefaults())); + .oauth2ResourceServer((oauth2) -> oauth2 + .jwt() + ); return http.build(); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java index 461f030e9f..babc68bc96 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java @@ -18,26 +18,20 @@ package org.springframework.security.config.annotation.web.configurers.saml2; import java.io.IOException; import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Collections; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import net.shibboleth.utilities.java.support.xml.SerializeSupport; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; -import org.opensaml.core.xml.io.Marshaller; -import org.opensaml.saml.saml2.core.Assertion; -import org.opensaml.saml.saml2.core.Response; -import org.w3c.dom.Element; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -63,7 +57,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextChangedListener; import org.springframework.security.core.context.SecurityContextHolderStrategy; -import org.springframework.security.saml2.core.OpenSamlInitializationService; import org.springframework.security.saml2.core.Saml2ErrorCodes; import org.springframework.security.saml2.core.Saml2Utils; import org.springframework.security.saml2.core.TestSaml2X509Credentials; @@ -73,7 +66,6 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken; -import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; @@ -101,6 +93,7 @@ import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.atLeastOnce; @@ -122,17 +115,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @ExtendWith(SpringTestContextExtension.class) public class Saml2LoginConfigurerTests { - static { - OpenSamlInitializationService.initialize(); - } - - private static final RelyingPartyRegistration registration = TestRelyingPartyRegistrations.noCredentials() - .signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential())) - .assertingPartyDetails((party) -> party - .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) - .build(); - - private static String SIGNED_RESPONSE; + private static final String SIGNED_RESPONSE = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9ycC5leGFtcGxlLm9yZy9hY3MiIElEPSJfYzE3MzM2YTAtNTM1My00MTQ5LWI3MmMtMDNkOWY5YWYzMDdlIiBJc3N1ZUluc3RhbnQ9IjIwMjAtMDgtMDRUMjI6MDQ6NDUuMDE2WiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KPGRzOlNpZ25lZEluZm8+CjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CjxkczpSZWZlcmVuY2UgVVJJPSIjX2MxNzMzNmEwLTUzNTMtNDE0OS1iNzJjLTAzZDlmOWFmMzA3ZSI+CjxkczpUcmFuc2Zvcm1zPgo8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgo8L2RzOlRyYW5zZm9ybXM+CjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz4KPGRzOkRpZ2VzdFZhbHVlPjYzTmlyenFzaDVVa0h1a3NuRWUrM0hWWU5aYWFsQW1OQXFMc1lGMlRuRDA9PC9kczpEaWdlc3RWYWx1ZT4KPC9kczpSZWZlcmVuY2U+CjwvZHM6U2lnbmVkSW5mbz4KPGRzOlNpZ25hdHVyZVZhbHVlPgpLMVlvWWJVUjBTclY4RTdVMkhxTTIvZUNTOTNoV25mOExnNnozeGZWMUlyalgzSXhWYkNvMVlYcnRBSGRwRVdvYTJKKzVOMmFNbFBHJiMxMzsKN2VpbDBZRC9xdUVRamRYbTNwQTBjZmEvY25pa2RuKzVhbnM0ZWQwanU1amo2dkpvZ2w2Smt4Q25LWUpwTU9HNzhtampmb0phengrWCYjMTM7CkM2NktQVStBYUdxeGVwUEQ1ZlhRdTFKSy9Jb3lBaitaa3k4Z2Jwc3VyZHFCSEJLRWxjdnVOWS92UGY0OGtBeFZBKzdtRGhNNUMvL1AmIzEzOwp0L084Y3NZYXB2UjZjdjZrdk45QXZ1N3FRdm9qVk1McHVxZWNJZDJwTUVYb0NSSnE2Nkd4MStNTUVPeHVpMWZZQlRoMEhhYjRmK3JyJiMxMzsKOEY2V1NFRC8xZllVeHliRkJqZ1Q4d2lEWHFBRU8wSVY4ZWRQeEE9PQo8L2RzOlNpZ25hdHVyZVZhbHVlPgo8L2RzOlNpZ25hdHVyZT48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iQWUzZjQ5OGI4LTliMTctNDA3OC05ZDM1LTg2YTA4NDA4NDk5NSIgSXNzdWVJbnN0YW50PSIyMDIwLTA4LTA0VDIyOjA0OjQ1LjA3N1oiIFZlcnNpb249IjIuMCI+PHNhbWwyOklzc3Vlcj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEPnRlc3RAc2FtbC51c2VyPC9zYW1sMjpOYW1lSUQ+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90QmVmb3JlPSIyMDIwLTA4LTA0VDIxOjU5OjQ1LjA5MFoiIE5vdE9uT3JBZnRlcj0iMjA0MC0wNy0zMFQyMjowNTowNi4wODhaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcnAuZXhhbXBsZS5vcmcvYWNzIi8+PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMjAtMDgtMDRUMjE6NTk6NDUuMDgwWiIgTm90T25PckFmdGVyPSIyMDQwLTA3LTMwVDIyOjA1OjA2LjA4N1oiLz48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4="; private static final AuthenticationConverter AUTHENTICATION_CONVERTER = mock(AuthenticationConverter.class); @@ -159,23 +142,6 @@ public class Saml2LoginConfigurerTests { private MockFilterChain filterChain; - @BeforeAll - static void createResponse() throws Exception { - String destination = registration.getAssertionConsumerServiceLocation(); - String assertingPartyEntityId = registration.getAssertingPartyDetails().getEntityId(); - String relyingPartyEntityId = registration.getEntityId(); - Response response = TestOpenSamlObjects.response(destination, assertingPartyEntityId); - Assertion assertion = TestOpenSamlObjects.assertion("test@saml.user", assertingPartyEntityId, - relyingPartyEntityId, destination); - response.getAssertions().add(assertion); - Response signed = TestOpenSamlObjects.signed(response, - registration.getSigningX509Credentials().iterator().next(), relyingPartyEntityId); - Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(signed); - Element element = marshaller.marshall(signed); - String serialized = SerializeSupport.nodeToString(element); - SIGNED_RESPONSE = Saml2Utils.samlEncode(serialized.getBytes(StandardCharsets.UTF_8)); - } - @BeforeEach public void setup() { this.request = new MockHttpServletRequest("POST", ""); @@ -344,9 +310,13 @@ public class Saml2LoginConfigurerTests { } @Test - public void saml2LoginWhenLoginProcessingUrlWithoutRegistrationIdAndDefaultAuthenticationConverterThenAutowires() - throws Exception { - this.spring.register(CustomLoginProcessingUrlDefaultAuthenticationConverter.class).autowire(); + public void saml2LoginWhenLoginProcessingUrlWithoutRegistrationIdAndDefaultAuthenticationConverterThenValidates() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy( + () -> this.spring.register(CustomLoginProcessingUrlDefaultAuthenticationConverter.class).autowire()) + .havingRootCause() + .isInstanceOf(IllegalStateException.class) + .withMessage("loginProcessingUrl must contain {registrationId} path variable"); } @Test @@ -414,7 +384,7 @@ public class Saml2LoginConfigurerTests { Authentication authentication = this.securityContextRepository .loadContext(new HttpRequestResponseHolder(this.request, this.response)) .getAuthentication(); - assertThat(authentication).as("Expected a valid authentication object.").isNotNull(); + Assertions.assertNotNull(authentication, "Expected a valid authentication object."); assertThat(authentication.getAuthorities()).hasSize(1); assertThat(authentication.getAuthorities()).first() .isInstanceOf(SimpleGrantedAuthority.class) @@ -746,6 +716,11 @@ public class Saml2LoginConfigurerTests { @Bean RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.noCredentials() + .signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartySigningCredential())) + .assertingPartyDetails((party) -> party.verificationX509Credentials( + (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) + .build(); return spy(new InMemoryRelyingPartyRegistrationRepository(registration)); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java deleted file mode 100644 index a7ba5b53de..0000000000 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurerTests.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web.configurers.saml2; - -import com.google.common.net.HttpHeaders; -import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.test.SpringTestContext; -import org.springframework.security.config.test.SpringTestContextExtension; -import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver; -import org.springframework.security.saml2.provider.service.metadata.RequestMatcherMetadataResponseResolver; -import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse; -import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver; -import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.test.web.servlet.MockMvc; - -import static org.hamcrest.Matchers.containsString; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Tests for {@link Saml2MetadataConfigurer} - */ -@ExtendWith(SpringTestContextExtension.class) -public class Saml2MetadataConfigurerTests { - - static RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); - - public final SpringTestContext spring = new SpringTestContext(this); - - @Autowired(required = false) - MockMvc mvc; - - @Test - void saml2MetadataRegistrationIdWhenDefaultsThenReturnsMetadata() throws Exception { - this.spring.register(DefaultConfig.class).autowire(); - String filename = "saml-" + registration.getRegistrationId() + "-metadata.xml"; - this.mvc.perform(get("/saml2/metadata/" + registration.getRegistrationId())) - .andExpect(status().isOk()) - .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString(filename))) - .andExpect(content().string(containsString("md:EntityDescriptor"))); - } - - @Test - void saml2MetadataRegistrationIdWhenWrongIdThenUnauthorized() throws Exception { - this.spring.register(DefaultConfig.class).autowire(); - this.mvc.perform(get("/saml2/metadata/" + registration.getRegistrationId() + "wrong")) - .andExpect(status().isUnauthorized()); - } - - @Test - void saml2MetadataWhenDefaultsThenReturnsMetadata() throws Exception { - this.spring.register(DefaultConfig.class).autowire(); - this.mvc.perform(get("/saml2/metadata")) - .andExpect(status().isOk()) - .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString("-metadata.xml"))) - .andExpect(content().string(containsString("md:EntityDescriptor"))); - } - - @Test - void saml2MetadataWhenMetadataResponseResolverThenUses() throws Exception { - this.spring.register(DefaultConfig.class, MetadataResponseResolverConfig.class).autowire(); - Saml2MetadataResponseResolver metadataResponseResolver = this.spring.getContext() - .getBean(Saml2MetadataResponseResolver.class); - given(metadataResponseResolver.resolve(any(HttpServletRequest.class))) - .willReturn(new Saml2MetadataResponse("metadata", "filename")); - this.mvc.perform(get("/saml2/metadata")) - .andExpect(status().isOk()) - .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString("filename"))) - .andExpect(content().string(containsString("metadata"))); - verify(metadataResponseResolver).resolve(any(HttpServletRequest.class)); - } - - @Test - void saml2MetadataWhenMetadataResponseResolverDslThenUses() throws Exception { - this.spring.register(MetadataResponseResolverDslConfig.class).autowire(); - this.mvc.perform(get("/saml2/metadata")) - .andExpect(status().isOk()) - .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString("filename"))) - .andExpect(content().string(containsString("metadata"))); - } - - @Test - void saml2MetadataWhenMetadataUrlThenUses() throws Exception { - this.spring.register(MetadataUrlConfig.class).autowire(); - String filename = "saml-" + registration.getRegistrationId() + "-metadata.xml"; - this.mvc.perform(get("/saml/metadata")) - .andExpect(status().isOk()) - .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString(filename))) - .andExpect(content().string(containsString("md:EntityDescriptor"))); - this.mvc.perform(get("/saml2/metadata")).andExpect(status().isForbidden()); - } - - @EnableWebSecurity - @Configuration - @Import(RelyingPartyRegistrationConfig.class) - static class DefaultConfig { - - @Bean - SecurityFilterChain filters(HttpSecurity http) throws Exception { - // @formatter:off - http - .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) - .saml2Metadata(Customizer.withDefaults()); - return http.build(); - // @formatter:on - } - - } - - @EnableWebSecurity - @Configuration - @Import(RelyingPartyRegistrationConfig.class) - static class MetadataUrlConfig { - - @Bean - SecurityFilterChain filters(HttpSecurity http) throws Exception { - // @formatter:off - http - .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) - .saml2Metadata((saml2) -> saml2.metadataUrl("/saml/metadata")); - return http.build(); - // @formatter:on - } - - // should ignore - @Bean - Saml2MetadataResponseResolver metadataResponseResolver(RelyingPartyRegistrationRepository registrations) { - return new RequestMatcherMetadataResponseResolver(registrations, new OpenSamlMetadataResolver()); - } - - } - - @EnableWebSecurity - @Configuration - @Import(RelyingPartyRegistrationConfig.class) - static class MetadataResponseResolverDslConfig { - - Saml2MetadataResponseResolver metadataResponseResolver = mock(Saml2MetadataResponseResolver.class); - - { - given(this.metadataResponseResolver.resolve(any(HttpServletRequest.class))) - .willReturn(new Saml2MetadataResponse("metadata", "filename")); - } - - @Bean - SecurityFilterChain filters(HttpSecurity http) throws Exception { - // @formatter:off - http - .authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) - .saml2Metadata((saml2) -> saml2.metadataResponseResolver(this.metadataResponseResolver)); - return http.build(); - // @formatter:on - } - - } - - @Configuration - static class MetadataResponseResolverConfig { - - Saml2MetadataResponseResolver metadataResponseResolver = mock(Saml2MetadataResponseResolver.class); - - @Bean - Saml2MetadataResponseResolver metadataResponseResolver() { - return this.metadataResponseResolver; - } - - } - - @Configuration - static class RelyingPartyRegistrationConfig { - - RelyingPartyRegistrationRepository registrations = new InMemoryRelyingPartyRegistrationRepository(registration); - - @Bean - RelyingPartyRegistrationRepository registrations() { - return this.registrations; - } - - } - -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurerTests.java index 89af68e9da..558362bcdd 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurerTests.java @@ -286,8 +286,8 @@ public class AbstractSecurityWebSocketMessageBrokerConfigurerTests { private void assertHandshake(HttpServletRequest request) { TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class); assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token); - assertThat(handshakeHandler.attributes).containsEntry(this.sessionAttr, - request.getSession().getAttribute(this.sessionAttr)); + assertThat(handshakeHandler.attributes.get(this.sessionAttr)) + .isEqualTo(request.getSession().getAttribute(this.sessionAttr)); } private HttpRequestHandler handler(HttpServletRequest request) throws Exception { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/socket/SyncExecutorSubscribableChannelPostProcessor.java b/config/src/test/java/org/springframework/security/config/annotation/web/socket/SyncExecutorSubscribableChannelPostProcessor.java index 36790510b9..fa1bf08d26 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/socket/SyncExecutorSubscribableChannelPostProcessor.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/socket/SyncExecutorSubscribableChannelPostProcessor.java @@ -27,7 +27,8 @@ public class SyncExecutorSubscribableChannelPostProcessor implements BeanPostPro @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof ExecutorSubscribableChannel original) { + if (bean instanceof ExecutorSubscribableChannel) { + ExecutorSubscribableChannel original = (ExecutorSubscribableChannel) bean; ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel(); channel.setInterceptors(original.getInterceptors()); return channel; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java index a14986b1ee..de58f72801 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java @@ -377,8 +377,8 @@ public class WebSocketMessageBrokerSecurityConfigurationTests { private void assertHandshake(HttpServletRequest request) { TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class); assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token); - assertThat(handshakeHandler.attributes).containsEntry(this.sessionAttr, - request.getSession().getAttribute(this.sessionAttr)); + assertThat(handshakeHandler.attributes.get(this.sessionAttr)) + .isEqualTo(request.getSession().getAttribute(this.sessionAttr)); } private HttpRequestHandler handler(HttpServletRequest request) throws Exception { @@ -633,8 +633,9 @@ public class WebSocketMessageBrokerSecurityConfigurationTests { public boolean doHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws HandshakeFailureException { this.attributes = attributes; - if (wsHandler instanceof SockJsWebSocketHandler sockJs) { + if (wsHandler instanceof SockJsWebSocketHandler) { // work around SPR-12716 + SockJsWebSocketHandler sockJs = (SockJsWebSocketHandler) wsHandler; WebSocketServerSockJsSession session = (WebSocketServerSockJsSession) ReflectionTestUtils .getField(sockJs, "sockJsSession"); this.attributes = session.getAttributes(); diff --git a/config/src/test/java/org/springframework/security/config/doc/SpringSecurityXsdParser.java b/config/src/test/java/org/springframework/security/config/doc/SpringSecurityXsdParser.java index 333b617c67..4cd696f468 100644 --- a/config/src/test/java/org/springframework/security/config/doc/SpringSecurityXsdParser.java +++ b/config/src/test/java/org/springframework/security/config/doc/SpringSecurityXsdParser.java @@ -181,7 +181,7 @@ public class SpringSecurityXsdParser { */ private Element elmt(XmlNode n) { String name = n.attribute("ref"); - if (!StringUtils.hasLength(name)) { + if (StringUtils.isEmpty(name)) { name = n.attribute("name"); } else { @@ -201,7 +201,7 @@ public class SpringSecurityXsdParser { e.getAttrs().forEach((attr) -> attr.setElmt(e)); e.getChildElmts().values().forEach((element) -> element.getParentElmts().put(e.getName(), e)); String subGrpName = n.attribute("substitutionGroup"); - if (StringUtils.hasLength(subGrpName)) { + if (!StringUtils.isEmpty(subGrpName)) { Element subGrp = elmt(findNode(n, subGrpName.split(":")[1])); subGrp.getSubGrps().add(e); } diff --git a/config/src/test/java/org/springframework/security/config/doc/XmlNode.java b/config/src/test/java/org/springframework/security/config/doc/XmlNode.java index 02e9096083..5ed6c38c0e 100644 --- a/config/src/test/java/org/springframework/security/config/doc/XmlNode.java +++ b/config/src/test/java/org/springframework/security/config/doc/XmlNode.java @@ -58,7 +58,8 @@ public class XmlNode { public Optional parent() { // @formatter:off - return Optional.ofNullable(this.node.getParentNode()).map(XmlNode::new); + return Optional.ofNullable(this.node.getParentNode()) + .map((parent) -> new XmlNode(parent)); // @formatter:on } @@ -66,7 +67,7 @@ public class XmlNode { // @formatter:off return Optional.ofNullable(this.node.getAttributes()) .map((attrs) -> attrs.getNamedItem(name)) - .map(Node::getTextContent) + .map((attr) -> attr.getTextContent()) .orElse(null); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java index 935c4e0cad..d487f62b4b 100644 --- a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java +++ b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,7 +65,7 @@ public class XsdDocumentedTests { String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd"; - String schemaDocumentLocation = "org/springframework/security/config/spring-security-6.2.xsd"; + String schemaDocumentLocation = "org/springframework/security/config/spring-security-6.0.xsd"; XmlSupport xml = new XmlSupport(); @@ -150,9 +150,8 @@ public class XsdDocumentedTests { .getParentFile() .list((dir, name) -> name.endsWith(".xsd")); // @formatter:on - assertThat(schemas.length) - .withFailMessage("the count is equal to 24, if not then schemaDocument needs updating") - .isEqualTo(24); + assertThat(schemas.length).isEqualTo(22) + .withFailMessage("the count is equal to 22, if not then schemaDocument needs updating"); } /** diff --git a/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java index 86a1c351ac..d501ee82bd 100644 --- a/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -63,7 +63,7 @@ public class FormLoginBeanDefinitionParserTests { + " \n" + " Please sign in\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
    \n" @@ -104,7 +104,7 @@ public class FormLoginBeanDefinitionParserTests { + " \n" + " Please sign in\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
    \n" diff --git a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java index 57741cea04..7a87a31037 100644 --- a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java @@ -417,7 +417,7 @@ public class MiscHttpConfigTests { this.spring.configLocations(xml("DeleteCookies")).autowire(); MvcResult result = this.mvc.perform(post("/logout").with(csrf())).andReturn(); List values = result.getResponse().getHeaders("Set-Cookie"); - assertThat(values).hasSize(2); + assertThat(values.size()).isEqualTo(2); assertThat(values).extracting((value) -> value.split("=")[0]).contains("JSESSIONID", "mycookie"); } diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests.java deleted file mode 100644 index 09c1c2dacd..0000000000 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests.java +++ /dev/null @@ -1,475 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.http; - -import java.time.Duration; -import java.time.Instant; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Consumer; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; -import org.springframework.security.config.test.SpringTestContext; -import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; -import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider; -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.OAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest; -import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -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.TestOAuth2RefreshTokens; -import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses; -import org.springframework.security.oauth2.jwt.JoseHeaderNames; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtClaimNames; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; -import org.springframework.util.StringUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link OAuth2AuthorizedClientManagerRegistrar}. - * - * @author Steve Riesenberg - */ -public class OAuth2AuthorizedClientManagerRegistrarTests { - - private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests"; - - private static OAuth2AccessTokenResponseClient MOCK_RESPONSE_CLIENT; - - public final SpringTestContext spring = new SpringTestContext(this); - - @Autowired - private OAuth2AuthorizedClientManager authorizedClientManager; - - @Autowired - private ClientRegistrationRepository clientRegistrationRepository; - - @Autowired - private OAuth2AuthorizedClientRepository authorizedClientRepository; - - @Autowired(required = false) - private AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider; - - private MockHttpServletRequest request; - - private MockHttpServletResponse response; - - @BeforeEach - @SuppressWarnings("unchecked") - public void setUp() { - MOCK_RESPONSE_CLIENT = mock(OAuth2AccessTokenResponseClient.class); - this.request = new MockHttpServletRequest(); - this.response = new MockHttpServletResponse(); - } - - @Test - public void loadContextWhenOAuth2ClientEnabledThenConfigured() { - this.spring.configLocations(xml("minimal")).autowire(); - assertThat(this.authorizedClientManager).isNotNull(); - } - - @Test - public void authorizeWhenAuthorizationCodeAuthorizedClientProviderBeanThenUsed() { - this.spring.configLocations(xml("providers")).autowire(); - - TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null); - // @formatter:off - OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest - .withClientRegistrationId("google") - .principal(authentication) - .attribute(HttpServletRequest.class.getName(), this.request) - .attribute(HttpServletResponse.class.getName(), this.response) - .build(); - assertThatExceptionOfType(ClientAuthorizationRequiredException.class) - .isThrownBy(() -> this.authorizedClientManager.authorize(authorizeRequest)) - .extracting(OAuth2AuthorizationException::getError) - .extracting(OAuth2Error::getErrorCode) - .isEqualTo("client_authorization_required"); - // @formatter:on - - verify(this.authorizationCodeAuthorizedClientProvider).authorize(any(OAuth2AuthorizationContext.class)); - } - - @Test - public void authorizeWhenRefreshTokenAccessTokenResponseClientBeanThenUsed() { - this.spring.configLocations(xml("clients")).autowire(); - testRefreshTokenGrant(); - } - - @Test - public void authorizeWhenRefreshTokenAuthorizedClientProviderBeanThenUsed() { - this.spring.configLocations(xml("providers")).autowire(); - testRefreshTokenGrant(); - } - - private void testRefreshTokenGrant() { - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); - given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class))) - .willReturn(accessTokenResponse); - - TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null); - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google"); - OAuth2AuthorizedClient existingAuthorizedClient = new OAuth2AuthorizedClient(clientRegistration, - authentication.getName(), getExpiredAccessToken(), TestOAuth2RefreshTokens.refreshToken()); - this.authorizedClientRepository.saveAuthorizedClient(existingAuthorizedClient, authentication, this.request, - this.response); - // @formatter:off - OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest - .withAuthorizedClient(existingAuthorizedClient) - .principal(authentication) - .attribute(HttpServletRequest.class.getName(), this.request) - .attribute(HttpServletResponse.class.getName(), this.response) - .build(); - // @formatter:on - OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest); - assertThat(authorizedClient).isNotNull(); - - ArgumentCaptor grantRequestCaptor = ArgumentCaptor - .forClass(OAuth2RefreshTokenGrantRequest.class); - verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); - - OAuth2RefreshTokenGrantRequest grantRequest = grantRequestCaptor.getValue(); - assertThat(grantRequest.getClientRegistration().getRegistrationId()) - .isEqualTo(clientRegistration.getRegistrationId()); - assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.REFRESH_TOKEN); - assertThat(grantRequest.getAccessToken()).isEqualTo(existingAuthorizedClient.getAccessToken()); - assertThat(grantRequest.getRefreshToken()).isEqualTo(existingAuthorizedClient.getRefreshToken()); - } - - @Test - public void authorizeWhenClientCredentialsAccessTokenResponseClientBeanThenUsed() { - this.spring.configLocations(xml("clients")).autowire(); - testClientCredentialsGrant(); - } - - @Test - public void authorizeWhenClientCredentialsAuthorizedClientProviderBeanThenUsed() { - this.spring.configLocations(xml("providers")).autowire(); - testClientCredentialsGrant(); - } - - private void testClientCredentialsGrant() { - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); - given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2ClientCredentialsGrantRequest.class))) - .willReturn(accessTokenResponse); - - TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", null); - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("github"); - // @formatter:off - OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest - .withClientRegistrationId(clientRegistration.getRegistrationId()) - .principal(authentication) - .attribute(HttpServletRequest.class.getName(), this.request) - .attribute(HttpServletResponse.class.getName(), this.response) - .build(); - // @formatter:on - OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest); - assertThat(authorizedClient).isNotNull(); - - ArgumentCaptor grantRequestCaptor = ArgumentCaptor - .forClass(OAuth2ClientCredentialsGrantRequest.class); - verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); - - OAuth2ClientCredentialsGrantRequest grantRequest = grantRequestCaptor.getValue(); - assertThat(grantRequest.getClientRegistration().getRegistrationId()) - .isEqualTo(clientRegistration.getRegistrationId()); - assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS); - } - - @Test - public void authorizeWhenPasswordAccessTokenResponseClientBeanThenUsed() { - this.spring.configLocations(xml("clients")).autowire(); - testPasswordGrant(); - } - - @Test - public void authorizeWhenPasswordAuthorizedClientProviderBeanThenUsed() { - this.spring.configLocations(xml("providers")).autowire(); - testPasswordGrant(); - } - - private void testPasswordGrant() { - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); - given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(OAuth2PasswordGrantRequest.class))) - .willReturn(accessTokenResponse); - - TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password"); - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("facebook"); - // @formatter:off - OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest - .withClientRegistrationId(clientRegistration.getRegistrationId()) - .principal(authentication) - .attribute(HttpServletRequest.class.getName(), this.request) - .attribute(HttpServletResponse.class.getName(), this.response) - .build(); - // @formatter:on - this.request.setParameter(OAuth2ParameterNames.USERNAME, "user"); - this.request.setParameter(OAuth2ParameterNames.PASSWORD, "password"); - OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest); - assertThat(authorizedClient).isNotNull(); - - ArgumentCaptor grantRequestCaptor = ArgumentCaptor - .forClass(OAuth2PasswordGrantRequest.class); - verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); - - OAuth2PasswordGrantRequest grantRequest = grantRequestCaptor.getValue(); - assertThat(grantRequest.getClientRegistration().getRegistrationId()) - .isEqualTo(clientRegistration.getRegistrationId()); - assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.PASSWORD); - assertThat(grantRequest.getUsername()).isEqualTo("user"); - assertThat(grantRequest.getPassword()).isEqualTo("password"); - } - - @Test - public void authorizeWhenJwtBearerAccessTokenResponseClientBeanThenUsed() { - this.spring.configLocations(xml("clients")).autowire(); - testJwtBearerGrant(); - } - - @Test - public void authorizeWhenJwtBearerAuthorizedClientProviderBeanThenUsed() { - this.spring.configLocations(xml("providers")).autowire(); - testJwtBearerGrant(); - } - - private void testJwtBearerGrant() { - OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); - given(MOCK_RESPONSE_CLIENT.getTokenResponse(any(JwtBearerGrantRequest.class))).willReturn(accessTokenResponse); - - JwtAuthenticationToken authentication = new JwtAuthenticationToken(getJwt()); - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("okta"); - // @formatter:off - OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest - .withClientRegistrationId(clientRegistration.getRegistrationId()) - .principal(authentication) - .attribute(HttpServletRequest.class.getName(), this.request) - .attribute(HttpServletResponse.class.getName(), this.response) - .build(); - // @formatter:on - OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest); - assertThat(authorizedClient).isNotNull(); - - ArgumentCaptor grantRequestCaptor = ArgumentCaptor.forClass(JwtBearerGrantRequest.class); - verify(MOCK_RESPONSE_CLIENT).getTokenResponse(grantRequestCaptor.capture()); - - JwtBearerGrantRequest grantRequest = grantRequestCaptor.getValue(); - assertThat(grantRequest.getClientRegistration().getRegistrationId()) - .isEqualTo(clientRegistration.getRegistrationId()); - assertThat(grantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.JWT_BEARER); - assertThat(grantRequest.getJwt().getSubject()).isEqualTo("user"); - } - - private static OAuth2AccessToken getExpiredAccessToken() { - Instant expiresAt = Instant.now().minusSeconds(60); - Instant issuedAt = expiresAt.minus(Duration.ofDays(1)); - return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "scopes", issuedAt, expiresAt, - new HashSet<>(Arrays.asList("read", "write"))); - } - - private static Jwt getJwt() { - Instant issuedAt = Instant.now(); - return new Jwt("token", issuedAt, issuedAt.plusSeconds(300), - Collections.singletonMap(JoseHeaderNames.ALG, "RS256"), - Collections.singletonMap(JwtClaimNames.SUB, "user")); - } - - private static String xml(String configName) { - return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; - } - - public static List getClientRegistrations() { - // @formatter:off - return Arrays.asList( - CommonOAuth2Provider.GOOGLE.getBuilder("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .build(), - CommonOAuth2Provider.GITHUB.getBuilder("github") - .clientId("github-client-id") - .clientSecret("github-client-secret") - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) - .build(), - CommonOAuth2Provider.FACEBOOK.getBuilder("facebook") - .clientId("facebook-client-id") - .clientSecret("facebook-client-secret") - .authorizationGrantType(AuthorizationGrantType.PASSWORD) - .build(), - CommonOAuth2Provider.OKTA.getBuilder("okta") - .clientId("okta-client-id") - .clientSecret("okta-client-secret") - .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) - .build()); - // @formatter:on - } - - public static Consumer authorizedClientManagerConsumer() { - return (authorizedClientManager) -> authorizedClientManager.setContextAttributesMapper((authorizeRequest) -> { - HttpServletRequest request = Objects - .requireNonNull(authorizeRequest.getAttribute(HttpServletRequest.class.getName())); - String username = request.getParameter(OAuth2ParameterNames.USERNAME); - String password = request.getParameter(OAuth2ParameterNames.PASSWORD); - - Map attributes = Collections.emptyMap(); - if (StringUtils.hasText(username) && StringUtils.hasText(password)) { - attributes = new HashMap<>(); - attributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); - attributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); - } - - return attributes; - }); - } - - public static AuthorizationCodeOAuth2AuthorizedClientProvider authorizationCodeAuthorizedClientProvider() { - return spy(new AuthorizationCodeOAuth2AuthorizedClientProvider()); - } - - public static RefreshTokenOAuth2AuthorizedClientProvider refreshTokenAuthorizedClientProvider() { - RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(refreshTokenAccessTokenResponseClient()); - return authorizedClientProvider; - } - - public static MockRefreshTokenClient refreshTokenAccessTokenResponseClient() { - return new MockRefreshTokenClient(); - } - - public static ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialsAuthorizedClientProvider() { - ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(clientCredentialsAccessTokenResponseClient()); - return authorizedClientProvider; - } - - public static OAuth2AccessTokenResponseClient clientCredentialsAccessTokenResponseClient() { - return new MockClientCredentialsClient(); - } - - public static PasswordOAuth2AuthorizedClientProvider passwordAuthorizedClientProvider() { - PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(passwordAccessTokenResponseClient()); - return authorizedClientProvider; - } - - public static OAuth2AccessTokenResponseClient passwordAccessTokenResponseClient() { - return new MockPasswordClient(); - } - - public static JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider() { - JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient()); - return authorizedClientProvider; - } - - public static OAuth2AccessTokenResponseClient jwtBearerAccessTokenResponseClient() { - return new MockJwtBearerClient(); - } - - private static class MockAuthorizationCodeClient - implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse( - OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockRefreshTokenClient - implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockClientCredentialsClient - implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse( - OAuth2ClientCredentialsGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockPasswordClient implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - - private static class MockJwtBearerClient implements OAuth2AccessTokenResponseClient { - - @Override - public OAuth2AccessTokenResponse getTokenResponse(JwtBearerGrantRequest authorizationGrantRequest) { - return MOCK_RESPONSE_CLIENT.getTokenResponse(authorizationGrantRequest); - } - - } - -} diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests.java index 3a84f768e6..022e5c684c 100644 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests.java @@ -24,9 +24,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.mock.web.MockHttpSession; import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; @@ -222,12 +219,8 @@ public class OAuth2ClientBeanDefinitionParserTests { ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google"); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, "user", TestOAuth2AccessTokens.noScopes()); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, request, response); - this.mvc.perform(get("/authorized-client").session((MockHttpSession) request.getSession())) - .andExpect(status().isOk()) - .andExpect(content().string("resolved")); + given(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).willReturn(authorizedClient); + this.mvc.perform(get("/authorized-client")).andExpect(status().isOk()).andExpect(content().string("resolved")); } private static OAuth2AuthorizationRequest createAuthorizationRequest(ClientRegistration clientRegistration) { diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java index b632f834de..5ffd87f868 100644 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java @@ -27,9 +27,6 @@ import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.event.AuthenticationSuccessEvent; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; @@ -536,11 +533,9 @@ public class OAuth2LoginBeanDefinitionParserTests { ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google-login"); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, "user", TestOAuth2AccessTokens.noScopes()); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, request, response); + given(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).willReturn(authorizedClient); // @formatter:off - this.mvc.perform(get("/authorized-client").session((MockHttpSession) request.getSession())) + this.mvc.perform(get("/authorized-client")) .andExpect(status().isOk()) .andExpect(content().string("resolved")); // @formatter:on @@ -555,7 +550,7 @@ public class OAuth2LoginBeanDefinitionParserTests { @GetMapping("/authorized-client") String authorizedClient(Model model, - @RegisteredOAuth2AuthorizedClient("google-login") OAuth2AuthorizedClient authorizedClient) { + @RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient authorizedClient) { return (authorizedClient != null) ? "resolved" : "not-resolved"; } diff --git a/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java index 9eb168233a..a01345e56b 100644 --- a/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests.java @@ -16,20 +16,11 @@ package org.springframework.security.config.http; -import java.nio.charset.StandardCharsets; - import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import net.shibboleth.utilities.java.support.xml.SerializeSupport; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; -import org.opensaml.core.xml.io.Marshaller; -import org.opensaml.saml.saml2.core.Assertion; -import org.opensaml.saml.saml2.core.Response; -import org.w3c.dom.Element; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; @@ -42,7 +33,6 @@ import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolderStrategy; -import org.springframework.security.saml2.core.OpenSamlInitializationService; import org.springframework.security.saml2.core.Saml2ParameterNames; import org.springframework.security.saml2.core.Saml2Utils; import org.springframework.security.saml2.core.TestSaml2X509Credentials; @@ -51,7 +41,6 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken; import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; -import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; @@ -90,19 +79,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @SecurityTestExecutionListeners public class Saml2LoginBeanDefinitionParserTests { - static { - OpenSamlInitializationService.initialize(); - } - private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests"; - private static final RelyingPartyRegistration registration = TestRelyingPartyRegistrations.noCredentials() - .signingX509Credentials((c) -> c.add(TestSaml2X509Credentials.assertingPartySigningCredential())) - .assertingPartyDetails((party) -> party - .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) - .build(); - - private static String SIGNED_RESPONSE; + private static final String SIGNED_RESPONSE = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9ycC5leGFtcGxlLm9yZy9hY3MiIElEPSJfYzE3MzM2YTAtNTM1My00MTQ5LWI3MmMtMDNkOWY5YWYzMDdlIiBJc3N1ZUluc3RhbnQ9IjIwMjAtMDgtMDRUMjI6MDQ6NDUuMDE2WiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KPGRzOlNpZ25lZEluZm8+CjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CjxkczpSZWZlcmVuY2UgVVJJPSIjX2MxNzMzNmEwLTUzNTMtNDE0OS1iNzJjLTAzZDlmOWFmMzA3ZSI+CjxkczpUcmFuc2Zvcm1zPgo8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgo8L2RzOlRyYW5zZm9ybXM+CjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz4KPGRzOkRpZ2VzdFZhbHVlPjYzTmlyenFzaDVVa0h1a3NuRWUrM0hWWU5aYWFsQW1OQXFMc1lGMlRuRDA9PC9kczpEaWdlc3RWYWx1ZT4KPC9kczpSZWZlcmVuY2U+CjwvZHM6U2lnbmVkSW5mbz4KPGRzOlNpZ25hdHVyZVZhbHVlPgpLMVlvWWJVUjBTclY4RTdVMkhxTTIvZUNTOTNoV25mOExnNnozeGZWMUlyalgzSXhWYkNvMVlYcnRBSGRwRVdvYTJKKzVOMmFNbFBHJiMxMzsKN2VpbDBZRC9xdUVRamRYbTNwQTBjZmEvY25pa2RuKzVhbnM0ZWQwanU1amo2dkpvZ2w2Smt4Q25LWUpwTU9HNzhtampmb0phengrWCYjMTM7CkM2NktQVStBYUdxeGVwUEQ1ZlhRdTFKSy9Jb3lBaitaa3k4Z2Jwc3VyZHFCSEJLRWxjdnVOWS92UGY0OGtBeFZBKzdtRGhNNUMvL1AmIzEzOwp0L084Y3NZYXB2UjZjdjZrdk45QXZ1N3FRdm9qVk1McHVxZWNJZDJwTUVYb0NSSnE2Nkd4MStNTUVPeHVpMWZZQlRoMEhhYjRmK3JyJiMxMzsKOEY2V1NFRC8xZllVeHliRkJqZ1Q4d2lEWHFBRU8wSVY4ZWRQeEE9PQo8L2RzOlNpZ25hdHVyZVZhbHVlPgo8L2RzOlNpZ25hdHVyZT48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iQWUzZjQ5OGI4LTliMTctNDA3OC05ZDM1LTg2YTA4NDA4NDk5NSIgSXNzdWVJbnN0YW50PSIyMDIwLTA4LTA0VDIyOjA0OjQ1LjA3N1oiIFZlcnNpb249IjIuMCI+PHNhbWwyOklzc3Vlcj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEPnRlc3RAc2FtbC51c2VyPC9zYW1sMjpOYW1lSUQ+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90QmVmb3JlPSIyMDIwLTA4LTA0VDIxOjU5OjQ1LjA5MFoiIE5vdE9uT3JBZnRlcj0iMjA0MC0wNy0zMFQyMjowNTowNi4wODhaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcnAuZXhhbXBsZS5vcmcvYWNzIi8+PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMjAtMDgtMDRUMjE6NTk6NDUuMDgwWiIgTm90T25PckFmdGVyPSIyMDQwLTA3LTMwVDIyOjA1OjA2LjA4N1oiLz48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4="; private static final String IDP_SSO_URL = "https://sso-url.example.com/IDP/SSO"; @@ -138,23 +117,6 @@ public class Saml2LoginBeanDefinitionParserTests { @Autowired private MockMvc mvc; - @BeforeAll - static void createResponse() throws Exception { - String destination = registration.getAssertionConsumerServiceLocation(); - String assertingPartyEntityId = registration.getAssertingPartyDetails().getEntityId(); - String relyingPartyEntityId = registration.getEntityId(); - Response response = TestOpenSamlObjects.response(destination, assertingPartyEntityId); - Assertion assertion = TestOpenSamlObjects.assertion("test@saml.user", assertingPartyEntityId, - relyingPartyEntityId, destination); - response.getAssertions().add(assertion); - Response signed = TestOpenSamlObjects.signed(response, - registration.getSigningX509Credentials().iterator().next(), relyingPartyEntityId); - Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(signed); - Element element = marshaller.marshall(signed); - String serialized = SerializeSupport.nodeToString(element); - SIGNED_RESPONSE = Saml2Utils.samlEncode(serialized.getBytes(StandardCharsets.UTF_8)); - } - @Test public void requestWhenSingleRelyingPartyRegistrationThenAutoRedirect() throws Exception { this.spring.configLocations(this.xml("SingleRelyingPartyRegistration")).autowire(); @@ -336,9 +298,13 @@ public class Saml2LoginBeanDefinitionParserTests { throws Exception { this.spring.configLocations(this.xml("WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter")) .autowire(); + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() + .assertingPartyDetails((party) -> party + .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) + .build(); String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); given(this.authenticationConverter.convert(any(HttpServletRequest.class))) - .willReturn(new Saml2AuthenticationToken(registration, response)); + .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); // @formatter:off MockHttpServletRequestBuilder request = post("/my/custom/url").param("SAMLResponse", SIGNED_RESPONSE); // @formatter:on @@ -347,8 +313,12 @@ public class Saml2LoginBeanDefinitionParserTests { } private RelyingPartyRegistration relyingPartyRegistrationWithVerifyingCredential() { - given(this.repository.findByRegistrationId(anyString())).willReturn(registration); - return registration; + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() + .assertingPartyDetails((party) -> party + .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) + .build(); + given(this.repository.findByRegistrationId(anyString())).willReturn(relyingPartyRegistration); + return relyingPartyRegistration; } private String xml(String configName) { diff --git a/config/src/test/java/org/springframework/security/config/test/SpringTestContextExtension.java b/config/src/test/java/org/springframework/security/config/test/SpringTestContextExtension.java index aeb4d37cc4..1496e52355 100644 --- a/config/src/test/java/org/springframework/security/config/test/SpringTestContextExtension.java +++ b/config/src/test/java/org/springframework/security/config/test/SpringTestContextExtension.java @@ -31,7 +31,7 @@ public class SpringTestContextExtension implements BeforeEachCallback, AfterEach @Override public void afterEach(ExtensionContext context) throws Exception { TestSecurityContextHolder.clearContext(); - getContexts(context.getRequiredTestInstance()).forEach(SpringTestContext::close); + getContexts(context.getRequiredTestInstance()).forEach((springTestContext) -> springTestContext.close()); } @Override diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java index d348d95f8a..3903ab5e06 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 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. @@ -39,7 +39,6 @@ import org.springframework.security.oauth2.client.registration.InMemoryReactiveC import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository; -import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.TestOAuth2AccessTokens; @@ -135,7 +134,6 @@ public class OAuth2ClientSpecTests { ServerAuthenticationConverter converter = config.authenticationConverter; ReactiveAuthenticationManager manager = config.manager; ServerAuthorizationRequestRepository authorizationRequestRepository = config.authorizationRequestRepository; - ServerOAuth2AuthorizationRequestResolver resolver = config.resolver; ServerRequestCache requestCache = config.requestCache; OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request() .redirectUri("/authorize/oauth2/code/registration-id") @@ -150,7 +148,6 @@ public class OAuth2ClientSpecTests { this.registration, authorizationExchange, accessToken); given(authorizationRequestRepository.loadAuthorizationRequest(any())) .willReturn(Mono.just(authorizationRequest)); - given(resolver.resolve(any())).willReturn(Mono.empty()); given(converter.convert(any())).willReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c"))); given(manager.authenticate(any())).willReturn(Mono.just(result)); given(requestCache.getRedirectUri(any())).willReturn(Mono.just(URI.create("/saved-request"))); @@ -168,7 +165,6 @@ public class OAuth2ClientSpecTests { verify(converter).convert(any()); verify(manager).authenticate(any()); verify(requestCache).getRedirectUri(any()); - verify(resolver).resolve(any()); } @Test @@ -275,8 +271,6 @@ public class OAuth2ClientSpecTests { ServerAuthorizationRequestRepository authorizationRequestRepository = mock( ServerAuthorizationRequestRepository.class); - ServerOAuth2AuthorizationRequestResolver resolver = mock(ServerOAuth2AuthorizationRequestResolver.class); - ServerRequestCache requestCache = mock(ServerRequestCache.class); @Bean @@ -287,7 +281,6 @@ public class OAuth2ClientSpecTests { .authenticationConverter(this.authenticationConverter) .authenticationManager(this.manager) .authorizationRequestRepository(this.authorizationRequestRepository) - .authorizationRequestResolver(this.resolver) .and() .requestCache((c) -> c.requestCache(this.requestCache)); // @formatter:on diff --git a/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java deleted file mode 100644 index 8117dc6e28..0000000000 --- a/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java +++ /dev/null @@ -1,685 +0,0 @@ -/* - * Copyright 2002-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server; - -import java.io.IOException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.interfaces.RSAPublicKey; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import com.gargoylesoftware.htmlunit.util.UrlUtils; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.source.ImmutableJWKSet; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.oauth2.sdk.Scope; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; -import com.nimbusds.openid.connect.sdk.token.OIDCTokens; -import jakarta.annotation.PreDestroy; -import okhttp3.mockwebserver.Dispatcher; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import reactor.core.publisher.Mono; - -import org.springframework.beans.factory.ObjectProvider; -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.context.annotation.Import; -import org.springframework.core.annotation.Order; -import org.springframework.http.ResponseCookie; -import org.springframework.http.client.reactive.ClientHttpConnector; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; -import org.springframework.security.config.test.SpringTestContext; -import org.springframework.security.config.test.SpringTestContextExtension; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; -import org.springframework.security.core.userdetails.ReactiveUserDetailsService; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimNames; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens; -import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry; -import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.TestClientRegistrations; -import org.springframework.security.oauth2.core.oidc.OidcIdToken; -import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.oauth2.jwt.JwtClaimsSet; -import org.springframework.security.oauth2.jwt.JwtEncoder; -import org.springframework.security.oauth2.jwt.JwtEncoderParameters; -import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; -import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; -import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher; -import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; -import org.springframework.test.web.reactive.server.FluxExchangeResult; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.test.web.reactive.server.WebTestClientConfigurer; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.ConfigurableWebApplicationContext; -import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.reactive.function.BodyInserters; -import org.springframework.web.server.WebSession; -import org.springframework.web.server.adapter.WebHttpHandlerBuilder; - -import static org.hamcrest.Matchers.containsString; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockAuthentication; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; - -/** - * Tests for {@link ServerHttpSecurity.OAuth2ResourceServerSpec} - */ -@ExtendWith({ SpringTestContextExtension.class }) -public class OidcLogoutSpecTests { - - private static final String SESSION_COOKIE_NAME = "SESSION"; - - private WebTestClient test; - - @Autowired(required = false) - private MockWebServer web; - - @Autowired - private ClientRegistration clientRegistration; - - public final SpringTestContext spring = new SpringTestContext(this); - - @Autowired - public void setApplicationContext(ApplicationContext context) { - this.test = WebTestClient.bindToApplicationContext(context) - .apply(springSecurity()) - .configureClient() - .responseTimeout(Duration.ofDays(1)) - .build(); - if (context instanceof ConfigurableWebApplicationContext configurable) { - configurable.getBeanFactory().registerResolvableDependency(WebTestClient.class, this.test); - } - } - - @Test - void logoutWhenDefaultsThenRemotelyInvalidatesSessions() { - this.spring.register(WebServerConfig.class, OidcProviderConfig.class, DefaultConfig.class).autowire(); - String registrationId = this.clientRegistration.getRegistrationId(); - String session = login(); - String logoutToken = this.test.mutateWith(session(session)) - .get() - .uri("/token/logout") - .exchange() - .expectStatus() - .isOk() - .returnResult(String.class) - .getResponseBody() - .blockFirst(); - this.test.post() - .uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) - .body(BodyInserters.fromFormData("logout_token", logoutToken)) - .exchange() - .expectStatus() - .isOk(); - this.test.mutateWith(session(session)).get().uri("/token/logout").exchange().expectStatus().isUnauthorized(); - } - - @Test - void logoutWhenInvalidLogoutTokenThenBadRequest() { - this.spring.register(WebServerConfig.class, OidcProviderConfig.class, DefaultConfig.class).autowire(); - this.test.get().uri("/token/logout").exchange().expectStatus().isUnauthorized(); - String registrationId = this.clientRegistration.getRegistrationId(); - FluxExchangeResult result = this.test.get() - .uri("/oauth2/authorization/" + registrationId) - .exchange() - .expectStatus() - .isFound() - .returnResult(String.class); - String session = sessionId(result); - String redirectUrl = UrlUtils.decode(result.getResponseHeaders().getLocation().toString()); - String state = this.test - .mutateWith(mockAuthentication(new TestingAuthenticationToken(this.clientRegistration.getClientId(), - this.clientRegistration.getClientSecret(), "APP"))) - .get() - .uri(redirectUrl) - .exchange() - .returnResult(String.class) - .getResponseBody() - .blockFirst(); - result = this.test.get() - .uri("/login/oauth2/code/" + registrationId + "?code=code&state=" + state) - .cookie("SESSION", session) - .exchange() - .expectStatus() - .isFound() - .returnResult(String.class); - session = sessionId(result); - this.test.post() - .uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) - .body(BodyInserters.fromFormData("logout_token", "invalid")) - .exchange() - .expectStatus() - .isBadRequest(); - this.test.get().uri("/token/logout").cookie("SESSION", session).exchange().expectStatus().isOk(); - } - - @Test - void logoutWhenLogoutTokenSpecifiesOneSessionThenRemotelyInvalidatesOnlyThatSession() throws Exception { - this.spring.register(WebServerConfig.class, OidcProviderConfig.class, DefaultConfig.class).autowire(); - String registrationId = this.clientRegistration.getRegistrationId(); - String one = login(); - String two = login(); - String three = login(); - String logoutToken = this.test.get() - .uri("/token/logout") - .cookie("SESSION", one) - .exchange() - .expectStatus() - .isOk() - .returnResult(String.class) - .getResponseBody() - .blockFirst(); - this.test.post() - .uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) - .body(BodyInserters.fromFormData("logout_token", logoutToken)) - .exchange() - .expectStatus() - .isOk(); - this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isUnauthorized(); - this.test.get().uri("/token/logout").cookie("SESSION", two).exchange().expectStatus().isOk(); - logoutToken = this.test.get() - .uri("/token/logout/all") - .cookie("SESSION", three) - .exchange() - .expectStatus() - .isOk() - .returnResult(String.class) - .getResponseBody() - .blockFirst(); - this.test.post() - .uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) - .body(BodyInserters.fromFormData("logout_token", logoutToken)) - .exchange() - .expectStatus() - .isOk(); - this.test.get().uri("/token/logout").cookie("SESSION", two).exchange().expectStatus().isUnauthorized(); - this.test.get().uri("/token/logout").cookie("SESSION", three).exchange().expectStatus().isUnauthorized(); - } - - @Test - void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() { - this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire(); - ServerLogoutHandler logoutHandler = this.spring.getContext().getBean(ServerLogoutHandler.class); - given(logoutHandler.logout(any(), any())).willReturn(Mono.error(() -> new IllegalStateException("illegal"))); - String registrationId = this.clientRegistration.getRegistrationId(); - String one = login(); - String logoutToken = this.test.get() - .uri("/token/logout/all") - .cookie("SESSION", one) - .exchange() - .expectStatus() - .isOk() - .returnResult(String.class) - .getResponseBody() - .blockFirst(); - this.test.post() - .uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) - .body(BodyInserters.fromFormData("logout_token", logoutToken)) - .exchange() - .expectStatus() - .isBadRequest() - .expectBody(String.class) - .value(containsString("partial_logout")); - this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk(); - } - - @Test - void logoutWhenCustomComponentsThenUses() { - this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithCustomComponentsConfig.class) - .autowire(); - String registrationId = this.clientRegistration.getRegistrationId(); - String sessionId = login(); - String logoutToken = this.test.get() - .uri("/token/logout") - .cookie("SESSION", sessionId) - .exchange() - .expectStatus() - .isOk() - .returnResult(String.class) - .getResponseBody() - .blockFirst(); - this.test.post() - .uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) - .body(BodyInserters.fromFormData("logout_token", logoutToken)) - .exchange() - .expectStatus() - .isOk(); - this.test.get().uri("/token/logout").cookie("SESSION", sessionId).exchange().expectStatus().isUnauthorized(); - ReactiveOidcSessionRegistry sessionRegistry = this.spring.getContext() - .getBean(ReactiveOidcSessionRegistry.class); - verify(sessionRegistry, atLeastOnce()).saveSessionInformation(any()); - verify(sessionRegistry, atLeastOnce()).removeSessionInformation(any(OidcLogoutToken.class)); - } - - private String login() { - this.test.get().uri("/token/logout").exchange().expectStatus().isUnauthorized(); - String registrationId = this.clientRegistration.getRegistrationId(); - FluxExchangeResult result = this.test.get() - .uri("/oauth2/authorization/" + registrationId) - .exchange() - .expectStatus() - .isFound() - .returnResult(String.class); - String sessionId = sessionId(result); - String redirectUrl = UrlUtils.decode(result.getResponseHeaders().getLocation().toString()); - result = this.test - .mutateWith(mockAuthentication(new TestingAuthenticationToken(this.clientRegistration.getClientId(), - this.clientRegistration.getClientSecret(), "APP"))) - .get() - .uri(redirectUrl) - .exchange() - .returnResult(String.class); - String state = result.getResponseBody().blockFirst(); - result = this.test.mutateWith(session(sessionId)) - .get() - .uri("/login/oauth2/code/" + registrationId + "?code=code&state=" + state) - .exchange() - .expectStatus() - .isFound() - .returnResult(String.class); - return sessionId(result); - } - - private String sessionId(FluxExchangeResult result) { - List cookies = result.getResponseCookies().get(SESSION_COOKIE_NAME); - if (cookies == null || cookies.isEmpty()) { - return null; - } - return cookies.get(0).getValue(); - } - - static SessionMutator session(String session) { - return new SessionMutator(session); - } - - private record SessionMutator(String session) implements WebTestClientConfigurer { - - @Override - public void afterConfigurerAdded(WebTestClient.Builder builder, WebHttpHandlerBuilder httpHandlerBuilder, - ClientHttpConnector connector) { - builder.defaultCookie(SESSION_COOKIE_NAME, this.session); - } - - } - - @Configuration - static class RegistrationConfig { - - @Autowired(required = false) - MockWebServer web; - - @Bean - ClientRegistration clientRegistration() { - if (this.web == null) { - return TestClientRegistrations.clientRegistration().build(); - } - String issuer = this.web.url("/").toString(); - return TestClientRegistrations.clientRegistration() - .issuerUri(issuer) - .jwkSetUri(issuer + "jwks") - .tokenUri(issuer + "token") - .userInfoUri(issuer + "user") - .scope("openid") - .build(); - } - - @Bean - ReactiveClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) { - return new InMemoryReactiveClientRegistrationRepository(clientRegistration); - } - - } - - @Configuration - @EnableWebFluxSecurity - @Import(RegistrationConfig.class) - static class DefaultConfig { - - @Bean - @Order(1) - SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception { - // @formatter:off - http - .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) - .oauth2Login(Customizer.withDefaults()) - .oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults())); - // @formatter:on - - return http.build(); - } - - } - - @Configuration - @EnableWebFluxSecurity - @Import(RegistrationConfig.class) - static class WithCustomComponentsConfig { - - ReactiveOidcSessionRegistry sessionRegistry = spy(new InMemoryReactiveOidcSessionRegistry()); - - @Bean - @Order(1) - SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception { - // @formatter:off - http - .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) - .oauth2Login((oauth2) -> oauth2.oidcSessionRegistry(this.sessionRegistry)) - .oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults())); - // @formatter:on - - return http.build(); - } - - @Bean - ReactiveOidcSessionRegistry sessionRegistry() { - return this.sessionRegistry; - } - - } - - @Configuration - @EnableWebFluxSecurity - @Import(RegistrationConfig.class) - static class WithBrokenLogoutConfig { - - private final ServerLogoutHandler logoutHandler = mock(ServerLogoutHandler.class); - - @Bean - @Order(1) - SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception { - // @formatter:off - http - .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) - .logout((logout) -> logout.logoutHandler(this.logoutHandler)) - .oauth2Login(Customizer.withDefaults()) - .oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults())); - // @formatter:on - - return http.build(); - } - - @Bean - ServerLogoutHandler logoutHandler() { - return this.logoutHandler; - } - - } - - @Configuration - @EnableWebFluxSecurity - @EnableWebFlux - @RestController - static class OidcProviderConfig { - - private static final RSAKey key = key(); - - private static final JWKSource jwks = jwks(key); - - private static RSAKey key() { - try { - KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); - return new RSAKey.Builder((RSAPublicKey) pair.getPublic()).privateKey(pair.getPrivate()).build(); - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - private static JWKSource jwks(RSAKey key) { - try { - return new ImmutableJWKSet<>(new JWKSet(key)); - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - private final String username = "user"; - - private final JwtEncoder encoder = new NimbusJwtEncoder(jwks); - - private String nonce; - - @Autowired - ClientRegistration registration; - - static ServerWebExchangeMatcher or(String... patterns) { - List matchers = new ArrayList<>(); - for (String pattern : patterns) { - matchers.add(new PathPatternParserServerWebExchangeMatcher(pattern)); - } - return new OrServerWebExchangeMatcher(matchers); - } - - @Bean - @Order(0) - SecurityWebFilterChain authorizationServer(ServerHttpSecurity http, ClientRegistration registration) - throws Exception { - // @formatter:off - http - .securityMatcher(or("/jwks", "/login/oauth/authorize", "/nonce", "/token", "/token/logout", "/user")) - .authorizeExchange((authorize) -> authorize - .pathMatchers("/jwks").permitAll() - .anyExchange().authenticated() - ) - .httpBasic(Customizer.withDefaults()) - .oauth2ResourceServer((oauth2) -> oauth2 - .jwt((jwt) -> jwt.jwkSetUri(registration.getProviderDetails().getJwkSetUri())) - ); - // @formatter:off - - return http.build(); - } - - @Bean - ReactiveUserDetailsService users(ClientRegistration registration) { - return new MapReactiveUserDetailsService(User.withUsername(registration.getClientId()) - .password("{noop}" + registration.getClientSecret()).authorities("APP").build()); - } - - @GetMapping("/login/oauth/authorize") - String nonce(@RequestParam("nonce") String nonce, @RequestParam("state") String state) { - this.nonce = nonce; - return state; - } - - @PostMapping("/token") - Map accessToken(WebSession session) { - JwtEncoderParameters parameters = JwtEncoderParameters - .from(JwtClaimsSet.builder().id("id").subject(this.username) - .issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now()) - .expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build()); - String token = this.encoder.encode(parameters).getTokenValue(); - return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null) - .toJSONObject(); - } - - String idToken(String sessionId) { - OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri()) - .subject(this.username).expiresAt(Instant.now().plusSeconds(86400)) - .audience(List.of(this.registration.getClientId())).nonce(this.nonce) - .claim(LogoutTokenClaimNames.SID, sessionId).build(); - JwtEncoderParameters parameters = JwtEncoderParameters - .from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build()); - return this.encoder.encode(parameters).getTokenValue(); - } - - @GetMapping("/user") - Map userinfo() { - return Map.of("sub", this.username, "id", this.username); - } - - @GetMapping("/jwks") - String jwks() { - return new JWKSet(key).toString(); - } - - @GetMapping("/token/logout") - String logoutToken(@AuthenticationPrincipal OidcUser user) { - OidcLogoutToken token = TestOidcLogoutTokens.withUser(user) - .audience(List.of(this.registration.getClientId())).build(); - JwtEncoderParameters parameters = JwtEncoderParameters - .from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build()); - return this.encoder.encode(parameters).getTokenValue(); - } - - @GetMapping("/token/logout/all") - String logoutTokenAll(@AuthenticationPrincipal OidcUser user) { - OidcLogoutToken token = TestOidcLogoutTokens.withUser(user) - .audience(List.of(this.registration.getClientId())) - .claims((claims) -> claims.remove(LogoutTokenClaimNames.SID)).build(); - JwtEncoderParameters parameters = JwtEncoderParameters - .from(JwtClaimsSet.builder().claims((claims) -> claims.putAll(token.getClaims())).build()); - return this.encoder.encode(parameters).getTokenValue(); - } - } - - @Configuration - static class WebServerConfig { - - private final MockWebServer server = new MockWebServer(); - - @Bean - MockWebServer web(ObjectProvider web) { - this.server.setDispatcher(new WebTestClientDispatcher(web)); - return this.server; - } - - @PreDestroy - void shutdown() throws IOException { - this.server.shutdown(); - } - - } - - private static class WebTestClientDispatcher extends Dispatcher { - - private final ObjectProvider webProvider; - - private WebTestClient web; - - WebTestClientDispatcher(ObjectProvider web) { - this.webProvider = web; - } - - @Override - public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - this.web = this.webProvider.getObject(); - String method = request.getMethod(); - String path = request.getPath(); - String csrf = request.getHeader("X-CSRF-TOKEN"); - String sessionId = session(request); - WebTestClient.RequestHeadersSpec r; - if ("GET".equals(method)) { - r = this.web.get().uri(path); - } - else { - WebTestClient.RequestBodySpec body; - if (csrf == null) { - body = this.web.mutateWith(csrf()).post().uri(path); - } - else { - body = this.web.post().uri(path).header("X-CSRF-TOKEN", csrf); - } - body.body(BodyInserters.fromValue(request.getBody().readUtf8())); - r = body; - } - for (Map.Entry> header : request.getHeaders().toMultimap().entrySet()) { - if (header.getKey().equalsIgnoreCase("Cookie")) { - continue; - } - r.header(header.getKey(), header.getValue().iterator().next()); - } - if (sessionId != null) { - r.cookie(SESSION_COOKIE_NAME, sessionId); - } - - try { - FluxExchangeResult result = r.exchange().returnResult(String.class); - return toMockResponse(result); - } - catch (Exception ex) { - MockResponse response = new MockResponse(); - response.setResponseCode(500); - response.setBody(ex.getMessage()); - return response; - } - } - - private String session(RecordedRequest request) { - String cookieHeaderValue = request.getHeader("Cookie"); - if (cookieHeaderValue == null) { - return null; - } - String[] cookies = cookieHeaderValue.split(";"); - for (String cookie : cookies) { - String[] parts = cookie.split("="); - if (SESSION_COOKIE_NAME.equals(parts[0])) { - return parts[1]; - } - } - return null; - } - - private MockResponse toMockResponse(FluxExchangeResult result) { - MockResponse response = new MockResponse(); - response.setResponseCode(result.getStatus().value()); - for (String name : result.getResponseHeaders().keySet()) { - response.addHeader(name, result.getResponseHeaders().getFirst(name)); - } - String body = result.getResponseBody().blockFirst(); - if (body != null) { - response.setBody(body); - } - return response; - } - - } - -} diff --git a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java index b8bcb40a67..872b1cf4fb 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java @@ -746,7 +746,7 @@ public class ServerHttpSecurityTests { private Optional getWebFilter(SecurityWebFilterChain filterChain, Class filterClass) { return (Optional) filterChain.getWebFilters() .filter(Objects::nonNull) - .filter((filter) -> filterClass.isAssignableFrom(filter.getClass())) + .filter((filter) -> filter.getClass().isAssignableFrom(filterClass)) .singleOrEmpty() .blockOptional(); } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests.kt index 32a3379b26..0ea704fe34 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests.kt @@ -18,17 +18,14 @@ package org.springframework.security.config.annotation.method.configuration import io.mockk.Called import io.mockk.clearAllMocks -import io.mockk.coEvery -import io.mockk.coVerify import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType -import org.junit.jupiter.api.AfterEach +import org.junit.After import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired @@ -49,7 +46,7 @@ class KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests { @Autowired var messageService: KotlinReactiveMessageService? = null - @AfterEach + @After fun cleanup() { clearAllMocks() } @@ -128,16 +125,6 @@ class KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests { verify { delegate wasNot Called } } - @Test - @WithMockUser(authorities = ["ROLE_ADMIN"]) - fun `suspendingPreAuthorizeDelegate when user has role then delegate called`() { - coEvery { delegate.suspendingPreAuthorizeHasRole() } returns "ok" - runBlocking { - messageService!!.suspendingPreAuthorizeDelegate() - } - coVerify(exactly = 1) { delegate.suspendingPreAuthorizeHasRole() } - } - @Test @WithMockUser(authorities = ["ROLE_ADMIN"]) fun `suspendingFlowPreAuthorize when user has role then success`() { @@ -181,16 +168,6 @@ class KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests { verify { delegate wasNot Called } } - @Test - @WithMockUser(authorities = ["ROLE_ADMIN"]) - fun `suspendingFlowPreAuthorizeDelegate when user has role then delegate called`() { - coEvery { delegate.flowPreAuthorize() } returns flow { } - runBlocking { - messageService!!.suspendingFlowPreAuthorizeDelegate().collect() - } - coVerify(exactly = 1) { delegate.flowPreAuthorize() } - } - @Test @WithMockUser(authorities = ["ROLE_ADMIN"]) fun `flowPreAuthorize when user has role then success`() { @@ -234,16 +211,6 @@ class KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests { verify { delegate wasNot Called } } - @Test - @WithMockUser(authorities = ["ROLE_ADMIN"]) - fun `flowPreAuthorizeDelegate when user has role then delegate called`() { - coEvery { delegate.flowPreAuthorize() } returns flow { } - runBlocking { - messageService!!.flowPreAuthorizeDelegate().collect() - } - coVerify(exactly = 1) { delegate.flowPreAuthorize() } - } - @Configuration @EnableReactiveMethodSecurity(useAuthorizationManager = false) open class Config { diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDslTests.kt index e341921325..3dac9f6ed9 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDslTests.kt @@ -124,13 +124,11 @@ class AuthorizeHttpRequestsDslTests { @RestController internal class PathController { @RequestMapping("/path") - fun path(): String { - return "ok" + fun path() { } @RequestMapping("/onlyPostPermitted") - fun onlyPostPermitted():String { - return "ok" + fun onlyPostPermitted() { } } } @@ -276,8 +274,7 @@ class AuthorizeHttpRequestsDslTests { @RestController internal class PathController { @GetMapping("/") - fun index(): String { - return "ok" + fun index() { } } @@ -343,8 +340,7 @@ class AuthorizeHttpRequestsDslTests { @RestController internal class PathController { @GetMapping("/") - fun index(): String { - return "ok" + fun index() { } } @@ -409,8 +405,7 @@ class AuthorizeHttpRequestsDslTests { @RestController internal class PathController { @GetMapping("/") - fun index(): String { - return "ok" + fun index() { } } @@ -476,8 +471,7 @@ class AuthorizeHttpRequestsDslTests { @RestController internal class PathController { @GetMapping("/") - fun index(): String { - return "ok" + fun index() { } } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/AuthorizeRequestsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/AuthorizeRequestsDslTests.kt index be39dbd89d..c4c3dcc067 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/AuthorizeRequestsDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/AuthorizeRequestsDslTests.kt @@ -114,13 +114,11 @@ class AuthorizeRequestsDslTests { @RestController internal class PathController { @RequestMapping("/path") - fun path(): String { - return "ok" + fun path() { } @RequestMapping("/onlyPostPermitted") - fun onlyPostPermitted(): String { - return "ok" + fun onlyPostPermitted() { } } } @@ -173,8 +171,7 @@ class AuthorizeRequestsDslTests { @RestController internal class PathController { @RequestMapping("/path") - fun path(): String { - return "ok" + fun path() { } } } @@ -330,8 +327,7 @@ class AuthorizeRequestsDslTests { @RestController internal class PathController { @GetMapping("/") - fun index():String { - return "ok" + fun index() { } } @@ -402,8 +398,7 @@ class AuthorizeRequestsDslTests { @RestController internal class PathController { @GetMapping("/") - fun index():String { - return "ok" + fun index() { } } @@ -466,8 +461,7 @@ class AuthorizeRequestsDslTests { @RestController internal class PathController { @RequestMapping("/path") - fun path():String { - return "ok" + fun path() { } } } @@ -490,8 +484,7 @@ class AuthorizeRequestsDslTests { @RestController internal class PathController { @RequestMapping("/path") - fun path(): String { - return "ok" + fun path() { } } } @@ -529,8 +522,7 @@ class AuthorizeRequestsDslTests { @RestController internal class PathController { @RequestMapping("/path") - fun path(): String { - return "ok" + fun path() { } } } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/CorsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/CorsDslTests.kt index 67574bc8a4..39adb26fa5 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/CorsDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/CorsDslTests.kt @@ -31,9 +31,7 @@ import org.springframework.security.config.test.SpringTestContextExtension import org.springframework.security.web.SecurityFilterChain import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.get -import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMethod -import org.springframework.web.bind.annotation.RestController import org.springframework.web.cors.CorsConfiguration import org.springframework.web.cors.CorsConfigurationSource import org.springframework.web.cors.UrlBasedCorsConfigurationSource @@ -74,7 +72,7 @@ class CorsDslTests { @Test fun `CORS when CORS configuration source bean then responds with CORS header`() { - this.spring.register(CorsCrossOriginBeanConfig::class.java, HomeController::class.java).autowire() + this.spring.register(CorsCrossOriginBeanConfig::class.java).autowire() this.mockMvc.get("/") { @@ -151,7 +149,7 @@ class CorsDslTests { @Test fun `CORS when CORS configuration source dsl then responds with CORS header`() { - this.spring.register(CorsCrossOriginBeanConfig::class.java, HomeController::class.java).autowire() + this.spring.register(CorsCrossOriginBeanConfig::class.java).autowire() this.mockMvc.get("/") { @@ -182,13 +180,4 @@ class CorsDslTests { return http.build() } } - - @RestController - private class HomeController { - @GetMapping("/") - fun ok(): String { - return "ok" - } - } - } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/CsrfDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/CsrfDslTests.kt index ae6568263d..ac2c78553c 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/CsrfDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/CsrfDslTests.kt @@ -290,13 +290,11 @@ class CsrfDslTests { @RestController internal class BasicController { @PostMapping("/test1") - fun test1():String { - return "ok" + fun test1() { } @PostMapping("/test2") - fun test2():String { - return "ok" + fun test2() { } } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/FormLoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/FormLoginDslTests.kt index 8c44ef8524..e150b82d56 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/FormLoginDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/FormLoginDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import io.mockk.verify import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired -import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.builders.HttpSecurity @@ -32,19 +31,19 @@ import org.springframework.security.config.test.SpringTestContext import org.springframework.security.config.test.SpringTestContextExtension import org.springframework.security.core.userdetails.User import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin -import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf -import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource import org.springframework.stereotype.Controller import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.get -import org.springframework.test.web.servlet.post import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.servlet.config.annotation.EnableWebMvc +import jakarta.servlet.http.HttpServletRequest +import org.springframework.context.annotation.Bean +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.WebAuthenticationDetails +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource /** * Tests for [FormLoginDsl] @@ -103,38 +102,6 @@ class FormLoginDslTests { } } - @Test - fun `request when formLogin disabled does not provide login page`() { - this.spring.register(DisabledConfig::class.java, UserConfig::class.java).autowire() - - this.mockMvc.get("/login") - .andExpect { - status { isNotFound() } - } - - this.mockMvc.post("/login") { - with(csrf()) - }.andExpect { - status { isNotFound() } - } - } - - @Configuration - @EnableWebMvc - @EnableWebSecurity - open class DisabledConfig { - @Bean - open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http.formLogin() - http { - formLogin { - disable() - } - } - return http.build() - } - } - @Test fun `request when secure then redirects to default login page`() { this.spring.register(AllSecuredConfig::class.java, UserConfig::class.java).autowire() diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpBasicDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpBasicDslTests.kt index da6ac0be36..2a1f1f6ad6 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpBasicDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpBasicDslTests.kt @@ -217,8 +217,7 @@ class HttpBasicDslTests { @RestController class MainController { @GetMapping("/") - fun main():String { - return "ok" + fun main() { } } } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDslTests.kt index ed11cf15a2..bdb849e83a 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ package org.springframework.security.config.annotation.web import io.mockk.every import io.mockk.mockkObject import io.mockk.verify -import jakarta.servlet.Filter import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -56,6 +55,7 @@ import org.springframework.test.web.servlet.get import org.springframework.test.web.servlet.post import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.web.servlet.config.annotation.EnableWebMvc +import jakarta.servlet.Filter /** * Tests for [HttpSecurityDsl] @@ -530,18 +530,6 @@ class HttpSecurityDslTests { ) } - @Test - fun `HTTP security when apply custom security configurer using with then custom filter added to filter chain`() { - this.spring.register(CustomSecurityConfigurerConfig::class.java).autowire() - - val filterChain = spring.context.getBean(FilterChainProxy::class.java) - val filterClasses: List> = filterChain.getFilters("/").map { it.javaClass } - - assertThat(filterClasses).contains( - CustomFilter::class.java - ) - } - @Configuration @EnableWebSecurity @EnableWebMvc @@ -557,21 +545,6 @@ class HttpSecurityDslTests { } } - @Configuration - @EnableWebSecurity - @EnableWebMvc - open class CustomSecurityConfigurerUsingWithConfig { - @Bean - open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - with(CustomSecurityConfigurer()) { - filter = CustomFilter() - } - } - return http.build() - } - } - class CustomSecurityConfigurer> : AbstractHttpConfigurer, H>() { var filter: Filter? = null override fun init(builder: H) { @@ -582,46 +555,4 @@ class HttpSecurityDslTests { builder.addFilterBefore(CustomFilter(), UsernamePasswordAuthenticationFilter::class.java) } } - - @Test - fun `HTTP security when apply form login using with from custom security configurer then filter added to filter chain`() { - this.spring.register(CustomDslUsingWithConfig::class.java).autowire() - - val filterChain = spring.context.getBean(FilterChainProxy::class.java) - val filterClasses: List> = filterChain.getFilters("/").map { it.javaClass } - - assertThat(filterClasses).contains( - UsernamePasswordAuthenticationFilter::class.java - ) - } - - @Configuration - @EnableWebSecurity - @EnableWebMvc - open class CustomDslUsingWithConfig { - @Bean - open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - with(CustomDslFormLogin()) { - formLogin = true - } - httpBasic { } - } - return http.build() - } - } - - class CustomDslFormLogin: AbstractHttpConfigurer() { - - var formLogin = false - - override fun init(builder: HttpSecurity) { - if (formLogin) { - builder.formLogin { } - } - } - - } - - } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OAuth2LoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OAuth2LoginDslTests.kt index cca0178e77..13102b63d9 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OAuth2LoginDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OAuth2LoginDslTests.kt @@ -128,9 +128,7 @@ class OAuth2LoginDslTests { @RestController class LoginController { @GetMapping("/custom-login") - fun loginPage():String { - return "ok" - } + fun loginPage() { } } } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt deleted file mode 100644 index 468be4251b..0000000000 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2002-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web - -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity -import org.springframework.security.config.test.SpringTestContext -import org.springframework.security.config.test.SpringTestContextExtension -import org.springframework.security.oauth2.client.registration.ClientRegistration -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository -import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository -import org.springframework.security.oauth2.client.registration.TestClientRegistrations -import org.springframework.security.web.SecurityFilterChain -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.post - -/** - * Tests for [OAuth2ClientDsl] - * - * @author Eleftheria Stein - */ -@ExtendWith(SpringTestContextExtension::class) -class OidcLogoutDslTests { - @JvmField - val spring = SpringTestContext(this) - - @Autowired - lateinit var mockMvc: MockMvc - - @Test - fun `oidcLogout when invalid token then errors`() { - this.spring.register(ClientRepositoryConfig::class.java).autowire() - val clientRegistration = this.spring.context.getBean(ClientRegistration::class.java) - this.mockMvc.post("/logout/connect/back-channel/" + clientRegistration.registrationId) { - param("logout_token", "token") - }.andExpect { status { isBadRequest() } } - } - - @Configuration - @EnableWebSecurity - open class ClientRepositoryConfig { - - @Bean - open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - oauth2Login { } - oidcLogout { - backChannel { } - } - authorizeHttpRequests { - authorize(anyRequest, authenticated) - } - } - return http.build() - } - - @Bean - open fun clientRegistration(): ClientRegistration { - return TestClientRegistrations.clientRegistration().build() - } - - @Bean - open fun clientRegistrationRepository(clientRegistration: ClientRegistration): ClientRegistrationRepository { - return InMemoryClientRegistrationRepository(clientRegistration) - } - } - -} diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/Saml2MetadataDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/Saml2MetadataDslTests.kt deleted file mode 100644 index ffb5e3bee1..0000000000 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/Saml2MetadataDslTests.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright 2002-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.annotation.web - -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.verify -import org.assertj.core.api.Assertions -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.springframework.beans.factory.BeanCreationException -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.core.io.ClassPathResource -import org.springframework.security.authentication.AuthenticationManager -import org.springframework.security.authentication.ProviderManager -import org.springframework.security.authentication.TestingAuthenticationProvider -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity -import org.springframework.security.config.test.SpringTestContext -import org.springframework.security.config.test.SpringTestContextExtension -import org.springframework.security.saml2.core.Saml2X509Credential -import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse -import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver -import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository -import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations -import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter -import org.springframework.security.web.SecurityFilterChain -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.get -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import java.security.cert.Certificate -import java.security.cert.CertificateFactory -import java.util.Base64 - -/** - * Tests for [Saml2Dsl] - * - * @author Eleftheria Stein - */ -@ExtendWith(SpringTestContextExtension::class) -class Saml2MetadataDslTests { - @JvmField - val spring = SpringTestContext(this) - - @Autowired - lateinit var mockMvc: MockMvc - - @Test - fun `saml2Metadat when no relying party registration repository then exception`() { - Assertions.assertThatThrownBy { this.spring.register(Saml2MetadataNoRelyingPartyRegistrationRepoConfig::class.java).autowire() } - .isInstanceOf(BeanCreationException::class.java) - .hasMessageContaining("relyingPartyRegistrationRepository cannot be null") - - } - - @Configuration - @EnableWebSecurity - open class Saml2MetadataNoRelyingPartyRegistrationRepoConfig { - @Bean - open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - saml2Metadata { } - } - return http.build() - } - } - - @Test - fun `metadata endpoint when saml2Metadata configured then metadata returned`() { - this.spring.register(Saml2MetadataConfig::class.java).autowire() - - this.mockMvc.get("/saml2/metadata") - .andExpect { - status { isOk() } - } - } - - @Configuration - @EnableWebSecurity - open class Saml2MetadataConfig { - - @Bean - open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - saml2Metadata { } - } - return http.build() - } - - @Bean - open fun registrations(): RelyingPartyRegistrationRepository { - return InMemoryRelyingPartyRegistrationRepository(TestRelyingPartyRegistrations.full().build()) - } - - private fun loadCert(location: String): T { - ClassPathResource(location).inputStream.use { inputStream -> - val certFactory = CertificateFactory.getInstance("X.509") - return certFactory.generateCertificate(inputStream) as T - } - } - } - - @Test - fun `metadata endpoint when url customized then used`() { - this.spring.register(Saml2LoginCustomEndpointConfig::class.java).autowire() - this.mockMvc.get("/saml/metadata") - .andExpect { - status { isOk() } - } - } - - @Configuration - @EnableWebSecurity - open class Saml2LoginCustomEndpointConfig { - @Bean - open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - saml2Metadata { - metadataUrl = "/saml/metadata" - } - } - return http.build() - } - - @Bean - open fun relyingPartyRegistrationRepository(): RelyingPartyRegistrationRepository? { - return InMemoryRelyingPartyRegistrationRepository(TestRelyingPartyRegistrations.full().build()) - } - } - - @Test - fun `metadata endpoint when resolver customized then used`() { - this.spring.register(Saml2LoginCustomMetadataResolverConfig::class.java).autowire() - val mocked = this.spring.context.getBean(Saml2MetadataResponseResolver::class.java) - every { - mocked.resolve(any()) - } returns Saml2MetadataResponse("metadata", "file") - this.mockMvc.get("/saml2/metadata") - .andExpect { - status { isOk() } - } - verify(exactly = 1) { mocked.resolve(any()) } - } - - @Configuration - @EnableWebSecurity - open class Saml2LoginCustomMetadataResolverConfig { - - private val metadataResponseResolver: Saml2MetadataResponseResolver = mockk() - - @Bean - open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - saml2Metadata {} - } - return http.build() - } - - @Bean - open fun metadataResponseResolver(): Saml2MetadataResponseResolver? { - return this.metadataResponseResolver - } - - @Bean - open fun relyingPartyRegistrationRepository(): RelyingPartyRegistrationRepository? { - return InMemoryRelyingPartyRegistrationRepository(TestRelyingPartyRegistrations.full().build()) - } - } -} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt deleted file mode 100644 index a4b62b2dc0..0000000000 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.config.web.server - -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -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.security.config.annotation.web.reactive.EnableWebFluxSecurity -import org.springframework.security.config.test.SpringTestContext -import org.springframework.security.config.test.SpringTestContextExtension -import org.springframework.security.oauth2.client.registration.ClientRegistration -import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository -import org.springframework.security.oauth2.client.registration.TestClientRegistrations -import org.springframework.security.web.server.SecurityWebFilterChain -import org.springframework.test.web.reactive.server.WebTestClient -import org.springframework.web.reactive.config.EnableWebFlux -import org.springframework.web.reactive.function.BodyInserters - -/** - * Tests for [ServerOidcLogoutDsl] - * - * @author Josh Cummings - */ -@ExtendWith(SpringTestContextExtension::class) -class ServerOidcLogoutDslTests { - @JvmField - val spring = SpringTestContext(this) - - private lateinit var client: WebTestClient - - @Autowired - fun setup(context: ApplicationContext) { - this.client = WebTestClient - .bindToApplicationContext(context) - .configureClient() - .build() - } - - @Test - fun `oidcLogout when invalid token then errors`() { - this.spring.register(ClientRepositoryConfig::class.java).autowire() - val clientRegistration = this.spring.context.getBean(ClientRegistration::class.java) - this.client.post() - .uri("/logout/connect/back-channel/" + clientRegistration.registrationId) - .body(BodyInserters.fromFormData("logout_token", "token")) - .exchange() - .expectStatus().isBadRequest - } - - @Configuration - @EnableWebFlux - @EnableWebFluxSecurity - open class ClientRepositoryConfig { - - @Bean - open fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - return http { - oauth2Login { } - oidcLogout { - backChannel { } - } - authorizeExchange { - authorize(anyExchange, authenticated) - } - } - } - - @Bean - open fun clientRegistration(): ClientRegistration { - return TestClientRegistrations.clientRegistration().build() - } - - @Bean - open fun clientRegistrationRepository(clientRegistration: ClientRegistration): ReactiveClientRegistrationRepository { - return InMemoryReactiveClientRegistrationRepository(clientRegistration) - } - } - -} diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-csrf-token-repository.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-csrf-token-repository.xml index e8ce3ecef0..572f6248b2 100644 --- a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-csrf-token-repository.xml +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-csrf-token-repository.xml @@ -21,8 +21,8 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-request-matcher.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-request-matcher.xml index 28ac9677bb..6e2d96b0ae 100644 --- a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-request-matcher.xml +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-mock-request-matcher.xml @@ -21,7 +21,7 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-AuthorizationManager.xml b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-AuthorizationManager.xml index 0b606b3a8f..981d7b6a8c 100644 --- a/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-AuthorizationManager.xml +++ b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-AuthorizationManager.xml @@ -29,8 +29,8 @@ - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-WithObservationRegistry.xml b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-WithObservationRegistry.xml index 0e3320619f..d1d1839f9b 100644 --- a/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-WithObservationRegistry.xml +++ b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-WithObservationRegistry.xml @@ -29,7 +29,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CollidingFilters.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CollidingFilters.xml index c46c8d47cf..e6a66cd5bf 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CollidingFilters.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CollidingFilters.xml @@ -29,7 +29,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CustomAuthenticationDetailsSourceRef.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CustomAuthenticationDetailsSourceRef.xml index 97fa2662c5..2dfdfde842 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CustomAuthenticationDetailsSourceRef.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CustomAuthenticationDetailsSourceRef.xml @@ -32,7 +32,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CustomFilters.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CustomFilters.xml index f547d25487..ec486f8422 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CustomFilters.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CustomFilters.xml @@ -34,7 +34,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CustomHttpBasicEntryPointRef.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CustomHttpBasicEntryPointRef.xml index 8168a807e9..47a0a16e01 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CustomHttpBasicEntryPointRef.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-CustomHttpBasicEntryPointRef.xml @@ -30,7 +30,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-EntryPoint.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-EntryPoint.xml index e9426cac31..d158b914bf 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-EntryPoint.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-EntryPoint.xml @@ -29,7 +29,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSaveAndExplicitRepository.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSaveAndExplicitRepository.xml index 53513aa671..4406065ff6 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSaveAndExplicitRepository.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExplicitSaveAndExplicitRepository.xml @@ -30,7 +30,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExpressionHandler.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExpressionHandler.xml index 7fd5edadb3..b7a8e6d1c0 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExpressionHandler.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-ExpressionHandler.xml @@ -35,7 +35,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-HttpFirewall.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-HttpFirewall.xml index 79b520a0bd..6bd7635c71 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-HttpFirewall.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-HttpFirewall.xml @@ -32,7 +32,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-Jaas.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-Jaas.xml index 0cd1a88f06..39f3597efc 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-Jaas.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-Jaas.xml @@ -48,7 +48,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-RequestCache.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-RequestCache.xml index 47ed006b15..f7fb16b64e 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-RequestCache.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-RequestCache.xml @@ -30,7 +30,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-RequestRejectedHandler.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-RequestRejectedHandler.xml index c6edc8d005..be62e9a47c 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-RequestRejectedHandler.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-RequestRejectedHandler.xml @@ -27,6 +27,6 @@ - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-Sec750.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-Sec750.xml index ffd22cd312..fd91fffdab 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-Sec750.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-Sec750.xml @@ -38,7 +38,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-SecurityContextRepository.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-SecurityContextRepository.xml index 0c38bb5943..859cddafd3 100644 --- a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-SecurityContextRepository.xml +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-SecurityContextRepository.xml @@ -30,7 +30,7 @@ - + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-clients.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-clients.xml deleted file mode 100644 index 416520c6f7..0000000000 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-clients.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-minimal.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-minimal.xml deleted file mode 100644 index 6efa77199a..0000000000 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-minimal.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-providers.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-providers.xml deleted file mode 100644 index 1966d46371..0000000000 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrarTests-providers.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml index 5c9737e9bf..228bdf3b54 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml @@ -43,7 +43,9 @@ provider-id="google"/> - + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomAuthorizationRedirectStrategy.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomAuthorizationRedirectStrategy.xml index 43ffb46ea9..4eadcb94fe 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomAuthorizationRedirectStrategy.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomAuthorizationRedirectStrategy.xml @@ -33,8 +33,8 @@ - - + + - - + + - - + + - - + + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomClientRegistrationRepository.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomClientRegistrationRepository.xml index d28b3b02d2..a3c5d3b091 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomClientRegistrationRepository.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomClientRegistrationRepository.xml @@ -30,8 +30,8 @@ - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomConfiguration.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomConfiguration.xml index 67ae60ee0c..dc15de679a 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomConfiguration.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomConfiguration.xml @@ -45,17 +45,17 @@ - - + + - - + + - - + + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml index d867ecfc46..dd0afc9351 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml @@ -34,7 +34,9 @@ - + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration-WithCustomConfiguration.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration-WithCustomConfiguration.xml index eb6c640004..0b4b8a4a20 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration-WithCustomConfiguration.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration-WithCustomConfiguration.xml @@ -33,20 +33,20 @@ - - + + - - + + - - + + - - + + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration-WithCustomGrantedAuthorities.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration-WithCustomGrantedAuthorities.xml index ee4d504803..f0f7cecc8d 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration-WithCustomGrantedAuthorities.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration-WithCustomGrantedAuthorities.xml @@ -35,23 +35,23 @@ - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration-WithCustomLoginProcessingUrl.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration-WithCustomLoginProcessingUrl.xml index c55d276c2c..8d075bc6fd 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration-WithCustomLoginProcessingUrl.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration-WithCustomLoginProcessingUrl.xml @@ -34,17 +34,17 @@ - - + + - - + + - - + + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationFailureHandler.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationFailureHandler.xml index 098f71a46e..4b5e241d76 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationFailureHandler.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationFailureHandler.xml @@ -30,8 +30,8 @@ - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRedirectStrategy.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRedirectStrategy.xml index cd6620d65c..8454de28f1 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRedirectStrategy.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRedirectStrategy.xml @@ -30,8 +30,8 @@ - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRequestResolver.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRequestResolver.xml index 6952e6e5db..f40da7c40e 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRequestResolver.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRequestResolver.xml @@ -30,8 +30,8 @@ - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactory.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactory.xml index 7a537e6858..e695599875 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactory.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactory.xml @@ -34,20 +34,20 @@ - - + + - - + + - - + + - - + + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactoryAndDefaultSuccessHandler.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactoryAndDefaultSuccessHandler.xml index bce09f2668..de5b7547ef 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactoryAndDefaultSuccessHandler.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactoryAndDefaultSuccessHandler.xml @@ -33,17 +33,17 @@ - - + + - - + + - - + + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration.xml index bf2b126d88..09a9b67c2e 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration.xml @@ -50,8 +50,8 @@ - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientRepository.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientRepository.xml index 4cd9409705..cef5a89a80 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientRepository.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientRepository.xml @@ -34,20 +34,20 @@ - - + + - - + + - - + + - - + + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientService.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientService.xml index 5ab6a590e0..e0c5787219 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientService.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientService.xml @@ -34,20 +34,20 @@ - - + + - - + + - - + + - - + + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomClientRegistrationRepository.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomClientRegistrationRepository.xml index 3f9133dcb9..d2f5947c4d 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomClientRegistrationRepository.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomClientRegistrationRepository.xml @@ -33,17 +33,17 @@ - - + + - - + + - - + + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomSecurityContextHolderStrategy.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomSecurityContextHolderStrategy.xml index 1b2ea12ffa..6af68c55fa 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomSecurityContextHolderStrategy.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomSecurityContextHolderStrategy.xml @@ -40,20 +40,20 @@ - - + + - - + + - - + + - - + + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationManagerResolver.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationManagerResolver.xml index 384ecdca84..2facf68726 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationManagerResolver.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationManagerResolver.xml @@ -22,7 +22,7 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationManagerResolverPlusOtherConfig.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationManagerResolverPlusOtherConfig.xml index 69f70fb41c..dbf45c1bd5 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationManagerResolverPlusOtherConfig.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationManagerResolverPlusOtherConfig.xml @@ -22,10 +22,10 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + - + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-ExpiredJwtClockSkew.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-ExpiredJwtClockSkew.xml index a9b1b9c0be..c132c7b98e 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-ExpiredJwtClockSkew.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-ExpiredJwtClockSkew.xml @@ -31,7 +31,7 @@ - + - + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-JwtRestOperations.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-JwtRestOperations.xml index d085cecd23..934e9809fb 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-JwtRestOperations.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-JwtRestOperations.xml @@ -22,7 +22,7 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + - + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockJwtAuthenticationConverter.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockJwtAuthenticationConverter.xml index 2e912b3f86..ab9785e7ea 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockJwtAuthenticationConverter.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockJwtAuthenticationConverter.xml @@ -22,6 +22,6 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockJwtDecoder.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockJwtDecoder.xml index 8cd8587fdc..341e1f1a6a 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockJwtDecoder.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockJwtDecoder.xml @@ -22,6 +22,6 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockJwtValidator.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockJwtValidator.xml index 14eeeac50c..764f5aaefe 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockJwtValidator.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockJwtValidator.xml @@ -22,11 +22,11 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + - + - + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml index b25d16ee9e..34efbd8f1f 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndAuthenticationConverter.xml @@ -21,8 +21,9 @@ xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndIntrospectionUri.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndIntrospectionUri.xml index 495847a5bc..21b470d7cb 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndIntrospectionUri.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenAndIntrospectionUri.xml @@ -22,7 +22,7 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenRestOperations.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenRestOperations.xml index 3f6f3aa182..5b855415f7 100644 --- a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenRestOperations.xml +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-OpaqueTokenRestOperations.xml @@ -22,7 +22,7 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + - + - + - + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler.xml index 81ae2a0930..fcd568996f 100644 --- a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler.xml +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration-WithCustomAuthenticationFailureHandler.xml @@ -28,10 +28,10 @@ - + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration.xml index a128f4c4e2..d694c40b12 100644 --- a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration.xml +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-SingleRelyingPartyRegistration.xml @@ -36,7 +36,7 @@ assertion-consumer-service-location="{baseUrl}/login/saml2/sso/{registrationId}" assertion-consumer-service-binding="REDIRECT" asserting-party-id="google"/> - + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter.xml index 47c98fb126..687f2700b0 100644 --- a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter.xml +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomLoginProcessingUrl-WithCustomAuthenticationConverter.xml @@ -28,11 +28,11 @@ - + - - - + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter.xml index 4783afa40c..2c8c8d620c 100644 --- a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter.xml +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationConverter.xml @@ -28,13 +28,13 @@ - + - - + + - - - + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationManager.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationManager.xml index dd08cc3eff..af5de241c3 100644 --- a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationManager.xml +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationManager.xml @@ -28,13 +28,13 @@ - + - - + + - - - + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver.xml index 8286c8fc8c..806a6fdb52 100644 --- a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver.xml +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthenticationRequestResolver.xml @@ -29,13 +29,13 @@ - + - - + + - - - + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository.xml index 0c0d5f8348..91e270febc 100644 --- a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository.xml +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository-WithCustomAuthnRequestRepository.xml @@ -28,13 +28,13 @@ - + - - + + - - - + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository.xml index 795faa7c11..5fc49103dc 100644 --- a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository.xml +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomRelyingPartyRepository.xml @@ -28,16 +28,16 @@ - + - - + + - - + + - - - + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomSecurityContextHolderStrategy.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomSecurityContextHolderStrategy.xml index 8f767c6762..b33a7da30e 100644 --- a/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomSecurityContextHolderStrategy.xml +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LoginBeanDefinitionParserTests-WithCustomSecurityContextHolderStrategy.xml @@ -36,14 +36,14 @@ - - + + - - + + - - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml index 2150d9bc55..7caa59eb92 100644 --- a/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CsrfDisabled-MockLogoutSuccessHandler.xml @@ -31,11 +31,11 @@ - + - - - + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml index 9ac53c5618..66068ed58d 100644 --- a/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml +++ b/config/src/test/resources/org/springframework/security/config/http/Saml2LogoutBeanDefinitionParserTests-CustomComponents.xml @@ -30,27 +30,27 @@ - + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + diff --git a/config/src/test/resources/org/springframework/security/config/method-security.xml b/config/src/test/resources/org/springframework/security/config/method-security.xml index 9551041262..d0f124b57c 100644 --- a/config/src/test/resources/org/springframework/security/config/method-security.xml +++ b/config/src/test/resources/org/springframework/security/config/method-security.xml @@ -6,7 +6,7 @@ xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd - http://www.springframework.org/schema/security org/springframework/security/config/spring-security-6.2.xsd"> + http://www.springframework.org/schema/security org/springframework/security/config/spring-security-6.0.xsd"> @@ -64,6 +64,6 @@ - - + + diff --git a/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomAuthorizationManagerConfig.xml b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomAuthorizationManagerConfig.xml index 0e7453cc6b..5827be1b75 100644 --- a/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomAuthorizationManagerConfig.xml +++ b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-CustomAuthorizationManagerConfig.xml @@ -23,7 +23,7 @@ - + diff --git a/core/spring-security-core.gradle b/core/spring-security-core.gradle index 57040811c0..fcedeea906 100644 --- a/core/spring-security-core.gradle +++ b/core/spring-security-core.gradle @@ -27,6 +27,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-params" testImplementation "org.junit.jupiter:junit-jupiter-engine" testImplementation "org.mockito:mockito-core" + testImplementation 'org.mockito:mockito-inline' testImplementation "org.mockito:mockito-junit-jupiter" testImplementation "org.springframework:spring-test" testImplementation 'org.skyscreamer:jsonassert' diff --git a/core/src/main/java/org/springframework/security/access/SecurityConfig.java b/core/src/main/java/org/springframework/security/access/SecurityConfig.java index 3079174e52..4be11c2dfc 100644 --- a/core/src/main/java/org/springframework/security/access/SecurityConfig.java +++ b/core/src/main/java/org/springframework/security/access/SecurityConfig.java @@ -38,7 +38,8 @@ public class SecurityConfig implements ConfigAttribute { @Override public boolean equals(Object obj) { - if (obj instanceof ConfigAttribute attr) { + if (obj instanceof ConfigAttribute) { + ConfigAttribute attr = (ConfigAttribute) obj; return this.attrib.equals(attr.getAttribute()); } return false; diff --git a/core/src/main/java/org/springframework/security/access/annotation/Jsr250MethodSecurityMetadataSource.java b/core/src/main/java/org/springframework/security/access/annotation/Jsr250MethodSecurityMetadataSource.java index 27992654d4..45f65bb84c 100644 --- a/core/src/main/java/org/springframework/security/access/annotation/Jsr250MethodSecurityMetadataSource.java +++ b/core/src/main/java/org/springframework/security/access/annotation/Jsr250MethodSecurityMetadataSource.java @@ -89,7 +89,8 @@ public class Jsr250MethodSecurityMetadataSource extends AbstractFallbackMethodSe attributes.add(Jsr250SecurityConfig.PERMIT_ALL_ATTRIBUTE); return attributes; } - if (annotation instanceof RolesAllowed ra) { + if (annotation instanceof RolesAllowed) { + RolesAllowed ra = (RolesAllowed) annotation; for (String allowed : ra.value()) { String defaultedAllowed = getRoleWithDefaultPrefix(allowed); diff --git a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java index a4a2aff309..de27a7d063 100644 --- a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java +++ b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.util.Assert; -import org.springframework.util.function.SingletonSupplier; /** * Base root object for use in Spring Security expression evaluations. @@ -87,11 +86,7 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat * @since 5.8 */ public SecurityExpressionRoot(Supplier authentication) { - this.authentication = SingletonSupplier.of(() -> { - Authentication value = authentication.get(); - Assert.notNull(value, "Authentication object cannot be null"); - return value; - }); + this.authentication = new AuthenticationSupplier(authentication); } @Override @@ -158,7 +153,7 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat @Override public final boolean isFullyAuthenticated() { Authentication authentication = getAuthentication(); - return this.trustResolver.isFullyAuthenticated(authentication); + return !this.trustResolver.isAnonymous(authentication) && !this.trustResolver.isRememberMe(authentication); } /** @@ -241,4 +236,27 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat return defaultRolePrefix + role; } + private static final class AuthenticationSupplier implements Supplier { + + private Authentication value; + + private final Supplier delegate; + + private AuthenticationSupplier(Supplier delegate) { + Assert.notNull(delegate, "delegate cannot be null"); + this.delegate = delegate; + } + + @Override + public Authentication get() { + if (this.value == null) { + Authentication authentication = this.delegate.get(); + Assert.notNull(authentication, "Authentication object cannot be null"); + this.value = authentication; + } + return this.value; + } + + } + } diff --git a/core/src/main/java/org/springframework/security/access/method/AbstractMethodSecurityMetadataSource.java b/core/src/main/java/org/springframework/security/access/method/AbstractMethodSecurityMetadataSource.java index c276ead8aa..219b3f7eb8 100644 --- a/core/src/main/java/org/springframework/security/access/method/AbstractMethodSecurityMetadataSource.java +++ b/core/src/main/java/org/springframework/security/access/method/AbstractMethodSecurityMetadataSource.java @@ -43,7 +43,8 @@ public abstract class AbstractMethodSecurityMetadataSource implements MethodSecu @Override public final Collection getAttributes(Object object) { - if (object instanceof MethodInvocation mi) { + if (object instanceof MethodInvocation) { + MethodInvocation mi = (MethodInvocation) object; Object target = mi.getThis(); Class targetClass = null; if (target != null) { diff --git a/core/src/main/java/org/springframework/security/access/method/MapBasedMethodSecurityMetadataSource.java b/core/src/main/java/org/springframework/security/access/method/MapBasedMethodSecurityMetadataSource.java index b7ebebb540..1c41d40d98 100644 --- a/core/src/main/java/org/springframework/security/access/method/MapBasedMethodSecurityMetadataSource.java +++ b/core/src/main/java/org/springframework/security/access/method/MapBasedMethodSecurityMetadataSource.java @@ -264,7 +264,8 @@ public class MapBasedMethodSecurityMetadataSource extends AbstractFallbackMethod if (this == obj) { return true; } - if (obj instanceof RegisteredMethod rhs) { + if (obj != null && obj instanceof RegisteredMethod) { + RegisteredMethod rhs = (RegisteredMethod) obj; return this.method.equals(rhs.method) && this.registeredJavaType.equals(rhs.registeredJavaType); } return false; diff --git a/core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java index 66e2e02f26..bd47472566 100644 --- a/core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/access/prepost/PrePostAdviceReactiveMethodInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ package org.springframework.security.access.prepost; import java.lang.reflect.Method; import java.util.Collection; +import kotlin.coroutines.Continuation; +import kotlinx.coroutines.reactive.AwaitKt; import kotlinx.coroutines.reactive.ReactiveFlowKt; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -27,6 +29,7 @@ import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.CoroutinesUtils; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapter; @@ -123,23 +126,34 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor .map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r)); } if (hasFlowReturnType) { + Flux response; if (isSuspendingFunction) { - return toInvoke - .flatMapMany((auth) -> Flux.from(PrePostAdviceReactiveMethodInterceptor.proceed(invocation)) - .map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r)); + response = toInvoke.flatMapMany((auth) -> Flux + .from(CoroutinesUtils.invokeSuspendingFunction(invocation.getMethod(), invocation.getThis(), + invocation.getArguments())) + .map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r)); } else { ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(returnType); Assert.state(adapter != null, () -> "The returnType " + returnType + " on " + method + " must have a org.springframework.core.ReactiveAdapter registered"); - Flux response = toInvoke.flatMapMany((auth) -> Flux + response = toInvoke.flatMapMany((auth) -> Flux .from(adapter.toPublisher(PrePostAdviceReactiveMethodInterceptor.flowProceed(invocation))) .map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r)); - return KotlinDelegate.asFlow(response); } + return KotlinDelegate.asFlow(response); } - return toInvoke.flatMap((auth) -> Mono.from(PrePostAdviceReactiveMethodInterceptor.proceed(invocation)) - .map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r)); + if (isSuspendingFunction) { + Mono response = toInvoke.flatMap((auth) -> Mono + .from(CoroutinesUtils.invokeSuspendingFunction(invocation.getMethod(), invocation.getThis(), + invocation.getArguments())) + .map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r)); + return KotlinDelegate.awaitSingleOrNull(response, + invocation.getArguments()[invocation.getArguments().length - 1]); + } + return toInvoke + .flatMapMany((auth) -> Flux.from(PrePostAdviceReactiveMethodInterceptor.>proceed(invocation)) + .map((r) -> (attr != null) ? this.postAdvice.after(auth, invocation, attr, r) : r)); } private static > T proceed(final MethodInvocation invocation) { @@ -187,6 +201,10 @@ public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor return ReactiveFlowKt.asFlow(publisher); } + private static Object awaitSingleOrNull(Publisher publisher, Object continuation) { + return AwaitKt.awaitSingleOrNull(publisher, (Continuation) continuation); + } + } } diff --git a/core/src/main/java/org/springframework/security/access/vote/ConsensusBased.java b/core/src/main/java/org/springframework/security/access/vote/ConsensusBased.java index b84cf64ffe..83f344d4ca 100644 --- a/core/src/main/java/org/springframework/security/access/vote/ConsensusBased.java +++ b/core/src/main/java/org/springframework/security/access/vote/ConsensusBased.java @@ -71,10 +71,14 @@ public class ConsensusBased extends AbstractAccessDecisionManager { for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, configAttributes); switch (result) { - case AccessDecisionVoter.ACCESS_GRANTED -> grant++; - case AccessDecisionVoter.ACCESS_DENIED -> deny++; - default -> { - } + case AccessDecisionVoter.ACCESS_GRANTED: + grant++; + break; + case AccessDecisionVoter.ACCESS_DENIED: + deny++; + break; + default: + break; } } if (grant > deny) { diff --git a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java index cfd066a912..78cec88812 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java @@ -68,14 +68,14 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre @Override public String getName() { - if (this.getPrincipal() instanceof UserDetails userDetails) { - return userDetails.getUsername(); + if (this.getPrincipal() instanceof UserDetails) { + return ((UserDetails) this.getPrincipal()).getUsername(); } - if (this.getPrincipal() instanceof AuthenticatedPrincipal authenticatedPrincipal) { - return authenticatedPrincipal.getName(); + if (this.getPrincipal() instanceof AuthenticatedPrincipal) { + return ((AuthenticatedPrincipal) this.getPrincipal()).getName(); } - if (this.getPrincipal() instanceof Principal principal) { - return principal.getName(); + if (this.getPrincipal() instanceof Principal) { + return ((Principal) this.getPrincipal()).getName(); } return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString(); } @@ -119,9 +119,10 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre @Override public boolean equals(Object obj) { - if (!(obj instanceof AbstractAuthenticationToken test)) { + if (!(obj instanceof AbstractAuthenticationToken)) { return false; } + AbstractAuthenticationToken test = (AbstractAuthenticationToken) obj; if (!this.authorities.equals(test.authorities)) { return false; } diff --git a/core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationToken.java index 2e2a47b7e4..2e92d105f7 100644 --- a/core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/AnonymousAuthenticationToken.java @@ -74,7 +74,8 @@ public class AnonymousAuthenticationToken extends AbstractAuthenticationToken im if (!super.equals(obj)) { return false; } - if (obj instanceof AnonymousAuthenticationToken test) { + if (obj instanceof AnonymousAuthenticationToken) { + AnonymousAuthenticationToken test = (AnonymousAuthenticationToken) obj; return (this.getKeyHash() == test.getKeyHash()); } return false; diff --git a/core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolver.java b/core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolver.java index 450f160332..b0de70d2ce 100644 --- a/core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolver.java +++ b/core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolver.java @@ -53,21 +53,4 @@ public interface AuthenticationTrustResolver { */ boolean isRememberMe(Authentication authentication); - /** - * Indicates whether the passed Authentication token represents a fully - * authenticated user (that is, neither anonymous or remember-me). This is a - * composition of isAnonymous and isRememberMe - * implementation - *

    - * @param authentication to test (may be null in which case the method - * will always return false) - * @return true the passed authentication token represented an anonymous - * principal and is authenticated using a remember-me token, false - * otherwise - * @since 6.1 - */ - default boolean isFullyAuthenticated(Authentication authentication) { - return !isAnonymous(authentication) && !isRememberMe(authentication); - } - } diff --git a/core/src/main/java/org/springframework/security/authentication/ObservationAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/ObservationAuthenticationManager.java index d6a021c6b3..377761be2b 100644 --- a/core/src/main/java/org/springframework/security/authentication/ObservationAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/ObservationAuthenticationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.security.authentication; import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.ObservationRegistry; import org.springframework.security.core.Authentication; @@ -36,7 +35,7 @@ public final class ObservationAuthenticationManager implements AuthenticationMan private final AuthenticationManager delegate; - private ObservationConvention convention = new AuthenticationObservationConvention(); + private final AuthenticationObservationConvention convention = new AuthenticationObservationConvention(); public ObservationAuthenticationManager(ObservationRegistry registry, AuthenticationManager delegate) { Assert.notNull(registry, "observationRegistry cannot be null"); @@ -57,15 +56,4 @@ public final class ObservationAuthenticationManager implements AuthenticationMan }); } - /** - * Use the provided convention for reporting observation data - * @param convention The provided convention - * - * @since 6.1 - */ - public void setObservationConvention(ObservationConvention convention) { - Assert.notNull(convention, "The observation convention cannot be null"); - this.convention = convention; - } - } diff --git a/core/src/main/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManager.java index 03eae7def0..be4eefeed1 100644 --- a/core/src/main/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,12 @@ package org.springframework.security.authentication; import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import reactor.core.publisher.Mono; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.util.Assert; /** * An {@link ReactiveAuthenticationManager} that observes the authentication @@ -38,7 +36,7 @@ public class ObservationReactiveAuthenticationManager implements ReactiveAuthent private final ReactiveAuthenticationManager delegate; - private ObservationConvention convention = new AuthenticationObservationConvention(); + private final AuthenticationObservationConvention convention = new AuthenticationObservationConvention(); public ObservationReactiveAuthenticationManager(ObservationRegistry registry, ReactiveAuthenticationManager delegate) { @@ -65,15 +63,4 @@ public class ObservationReactiveAuthenticationManager implements ReactiveAuthent }); } - /** - * Use the provided convention for reporting observation data - * @param convention The provided convention - * - * @since 6.1 - */ - public void setObservationConvention(ObservationConvention convention) { - Assert.notNull(convention, "The observation convention cannot be null"); - this.convention = convention; - } - } diff --git a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java index a09283b08c..bc54e4c026 100644 --- a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java +++ b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java @@ -256,7 +256,8 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar * @param dest the destination authentication object */ private void copyDetails(Authentication source, Authentication dest) { - if ((dest instanceof AbstractAuthenticationToken token) && (dest.getDetails() == null)) { + if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) { + AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest; token.setDetails(source.getDetails()); } } diff --git a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java index 629372bc1a..8a62c9ca2a 100644 --- a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java @@ -94,8 +94,12 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken { if (!super.equals(obj)) { return false; } - if (obj instanceof RememberMeAuthenticationToken other) { - return this.getKeyHash() == other.getKeyHash(); + if (obj instanceof RememberMeAuthenticationToken) { + RememberMeAuthenticationToken other = (RememberMeAuthenticationToken) obj; + if (this.getKeyHash() != other.getKeyHash()) { + return false; + } + return true; } return false; } diff --git a/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java index abfc6560f4..8162b45965 100644 --- a/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java @@ -16,7 +16,6 @@ package org.springframework.security.authentication; -import java.util.Collection; import java.util.List; import org.springframework.security.core.GrantedAuthority; @@ -48,13 +47,7 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken { this(principal, credentials, AuthorityUtils.createAuthorityList(authorities)); } - public TestingAuthenticationToken(Object principal, Object credentials, - List authorities) { - this(principal, credentials, (Collection) authorities); - } - - public TestingAuthenticationToken(Object principal, Object credentials, - Collection authorities) { + public TestingAuthenticationToken(Object principal, Object credentials, List authorities) { super(authorities); this.principal = principal; this.credentials = credentials; diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java index b886caf6f1..32e7ea70b7 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/AbstractJaasAuthenticationProvider.java @@ -160,9 +160,10 @@ public abstract class AbstractJaasAuthenticationProvider implements Authenticati */ @Override public Authentication authenticate(Authentication auth) throws AuthenticationException { - if (!(auth instanceof UsernamePasswordAuthenticationToken request)) { + if (!(auth instanceof UsernamePasswordAuthenticationToken)) { return null; } + UsernamePasswordAuthenticationToken request = (UsernamePasswordAuthenticationToken) auth; Set authorities; try { // Create the LoginContext object, and pass our InternallCallbackHandler @@ -232,7 +233,8 @@ public abstract class AbstractJaasAuthenticationProvider implements Authenticati } for (SecurityContext context : contexts) { Authentication auth = context.getAuthentication(); - if ((auth instanceof JaasAuthenticationToken token)) { + if ((auth != null) && (auth instanceof JaasAuthenticationToken)) { + JaasAuthenticationToken token = (JaasAuthenticationToken) auth; try { LoginContext loginContext = token.getLoginContext(); logout(token, loginContext); diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/JaasGrantedAuthority.java b/core/src/main/java/org/springframework/security/authentication/jaas/JaasGrantedAuthority.java index 8096947840..d0d76b89d1 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/JaasGrantedAuthority.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/JaasGrantedAuthority.java @@ -58,8 +58,9 @@ public final class JaasGrantedAuthority implements GrantedAuthority { if (this == obj) { return true; } - if (obj instanceof JaasGrantedAuthority jga) { - return this.role.equals(jga.getAuthority()) && this.principal.equals(jga.getPrincipal()); + if (obj instanceof JaasGrantedAuthority) { + JaasGrantedAuthority jga = (JaasGrantedAuthority) obj; + return this.role.equals(jga.role) && this.principal.equals(jga.principal); } return false; } diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/SecurityContextLoginModule.java b/core/src/main/java/org/springframework/security/authentication/jaas/SecurityContextLoginModule.java index ca88f0aacf..096e298a8a 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/SecurityContextLoginModule.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/SecurityContextLoginModule.java @@ -71,6 +71,7 @@ public class SecurityContextLoginModule implements LoginModule { * Authentication. * @return true if this method succeeded, or false if this LoginModule * should be ignored. + * @exception LoginException if the abort fails */ @Override public boolean abort() { @@ -86,6 +87,7 @@ public class SecurityContextLoginModule implements LoginModule { * Authentication to the Subject's principals. * @return true if this method succeeded, or false if this LoginModule * should be ignored. + * @exception LoginException if the commit fails */ @Override public boolean commit() { @@ -159,6 +161,7 @@ public class SecurityContextLoginModule implements LoginModule { * Log out the Subject. * @return true if this method succeeded, or false if this LoginModule * should be ignored. + * @exception LoginException if the logout fails */ @Override public boolean logout() { diff --git a/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java index 88192e4d9e..b9236eae3d 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthenticatedAuthorizationManager.java @@ -143,7 +143,7 @@ public final class AuthenticatedAuthorizationManager implements Authorization @Override boolean isGranted(Authentication authentication) { - return authentication != null && this.trustResolver.isFullyAuthenticated(authentication); + return super.isGranted(authentication) && !this.trustResolver.isRememberMe(authentication); } } diff --git a/core/src/main/java/org/springframework/security/authorization/AuthoritiesAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthoritiesAuthorizationManager.java deleted file mode 100644 index eedb9d8671..0000000000 --- a/core/src/main/java/org/springframework/security/authorization/AuthoritiesAuthorizationManager.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2002-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.authorization; - -import java.util.Collection; -import java.util.function.Supplier; - -import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; -import org.springframework.security.access.hierarchicalroles.RoleHierarchy; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.util.Assert; - -/** - * An {@link AuthorizationManager} that determines if the current user is authorized by - * evaluating if the {@link Authentication} contains any of the specified authorities. - * - * @author Evgeniy Cheban - * @since 6.1 - */ -public final class AuthoritiesAuthorizationManager implements AuthorizationManager> { - - private RoleHierarchy roleHierarchy = new NullRoleHierarchy(); - - /** - * Sets the {@link RoleHierarchy} to be used. Default is {@link NullRoleHierarchy}. - * Cannot be null. - * @param roleHierarchy the {@link RoleHierarchy} to use - */ - public void setRoleHierarchy(RoleHierarchy roleHierarchy) { - Assert.notNull(roleHierarchy, "roleHierarchy cannot be null"); - this.roleHierarchy = roleHierarchy; - } - - /** - * Determines if the current user is authorized by evaluating if the - * {@link Authentication} contains any of specified authorities. - * @param authentication the {@link Supplier} of the {@link Authentication} to check - * @param authorities the collection of authority strings to check - * @return an {@link AuthorityAuthorizationDecision} - */ - @Override - public AuthorityAuthorizationDecision check(Supplier authentication, - Collection authorities) { - boolean granted = isGranted(authentication.get(), authorities); - return new AuthorityAuthorizationDecision(granted, AuthorityUtils.createAuthorityList(authorities)); - } - - private boolean isGranted(Authentication authentication, Collection authorities) { - return authentication != null && isAuthorized(authentication, authorities); - } - - private boolean isAuthorized(Authentication authentication, Collection authorities) { - for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) { - if (authorities.contains(grantedAuthority.getAuthority())) { - return true; - } - } - return false; - } - - private Collection getGrantedAuthorities(Authentication authentication) { - return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()); - } - -} diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java index 1d93dffa08..bf95c18fb8 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java @@ -16,12 +16,16 @@ package org.springframework.security.authorization; +import java.util.Collection; +import java.util.List; import java.util.Set; import java.util.function.Supplier; import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.util.Assert; /** @@ -36,12 +40,12 @@ public final class AuthorityAuthorizationManager implements AuthorizationMana private static final String ROLE_PREFIX = "ROLE_"; - private final AuthoritiesAuthorizationManager delegate = new AuthoritiesAuthorizationManager(); + private final List authorities; - private final Set authorities; + private RoleHierarchy roleHierarchy = new NullRoleHierarchy(); private AuthorityAuthorizationManager(String... authorities) { - this.authorities = Set.of(authorities); + this.authorities = AuthorityUtils.createAuthorityList(authorities); } /** @@ -51,7 +55,8 @@ public final class AuthorityAuthorizationManager implements AuthorizationMana * @since 5.8 */ public void setRoleHierarchy(RoleHierarchy roleHierarchy) { - this.delegate.setRoleHierarchy(roleHierarchy); + Assert.notNull(roleHierarchy, "roleHierarchy cannot be null"); + this.roleHierarchy = roleHierarchy; } /** @@ -142,7 +147,26 @@ public final class AuthorityAuthorizationManager implements AuthorizationMana */ @Override public AuthorizationDecision check(Supplier authentication, T object) { - return this.delegate.check(authentication, this.authorities); + boolean granted = isGranted(authentication.get()); + return new AuthorityAuthorizationDecision(granted, this.authorities); + } + + private boolean isGranted(Authentication authentication) { + return authentication != null && authentication.isAuthenticated() && isAuthorized(authentication); + } + + private boolean isAuthorized(Authentication authentication) { + Set authorities = AuthorityUtils.authorityListToSet(this.authorities); + for (GrantedAuthority grantedAuthority : getGrantedAuthorities(authentication)) { + if (authorities.contains(grantedAuthority.getAuthority())) { + return true; + } + } + return false; + } + + private Collection getGrantedAuthorities(Authentication authentication) { + return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()); } @Override diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java index 221d5f01c6..a2a502f0e6 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java @@ -26,7 +26,7 @@ import org.springframework.security.core.Authentication; * An Authorization manager which can determine if an {@link Authentication} has access to * a specific object. * - * @param the type of object that the authorization check is being done on. + * @param the type of object that the authorization check is being done one. * @author Evgeniy Cheban */ @FunctionalInterface diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationConvention.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationConvention.java index c011835aab..0b8e618202 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationConvention.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationObservationConvention.java @@ -95,9 +95,6 @@ public final class AuthorizationObservationConvention if (className.contains("Message")) { return "message"; } - if (className.contains("Exchange")) { - return "exchange"; - } return className; } diff --git a/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java index 943e46dce3..343762e0ac 100644 --- a/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,16 +19,10 @@ package org.springframework.security.authorization; import java.util.function.Supplier; import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.ObservationRegistry; -import org.springframework.context.MessageSource; -import org.springframework.context.MessageSourceAware; -import org.springframework.context.support.MessageSourceAccessor; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; -import org.springframework.security.core.SpringSecurityMessageSource; -import org.springframework.util.Assert; /** * An {@link AuthorizationManager} that observes the authorization @@ -36,15 +30,13 @@ import org.springframework.util.Assert; * @author Josh Cummings * @since 6.0 */ -public final class ObservationAuthorizationManager implements AuthorizationManager, MessageSourceAware { +public final class ObservationAuthorizationManager implements AuthorizationManager { private final ObservationRegistry registry; private final AuthorizationManager delegate; - private ObservationConvention> convention = new AuthorizationObservationConvention(); - - private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); + private final AuthorizationObservationConvention convention = new AuthorizationObservationConvention(); public ObservationAuthorizationManager(ObservationRegistry registry, AuthorizationManager delegate) { this.registry = registry; @@ -63,8 +55,7 @@ public final class ObservationAuthorizationManager implements AuthorizationMa AuthorizationDecision decision = this.delegate.check(wrapped, object); context.setDecision(decision); if (decision != null && !decision.isGranted()) { - observation.error(new AccessDeniedException( - this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access Denied"))); + observation.error(new AccessDeniedException("Access Denied")); } return decision; } @@ -77,25 +68,4 @@ public final class ObservationAuthorizationManager implements AuthorizationMa } } - /** - * Use the provided convention for reporting observation data - * @param convention The provided convention - * - * @since 6.1 - */ - public void setObservationConvention(ObservationConvention> convention) { - Assert.notNull(convention, "The observation convention cannot be null"); - this.convention = convention; - } - - /** - * Set the MessageSource that this object runs in. - * @param messageSource The message source to be used by this object - * @since 6.2 - */ - @Override - public void setMessageSource(final MessageSource messageSource) { - this.messages = new MessageSourceAccessor(messageSource); - } - } diff --git a/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java index 83f4cb0609..b9d1158304 100644 --- a/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,12 @@ package org.springframework.security.authorization; import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import reactor.core.publisher.Mono; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; -import org.springframework.util.Assert; /** * An {@link ReactiveAuthorizationManager} that observes the authentication @@ -38,7 +36,7 @@ public final class ObservationReactiveAuthorizationManager implements Reactiv private final ReactiveAuthorizationManager delegate; - private ObservationConvention> convention = new AuthorizationObservationConvention(); + private final AuthorizationObservationConvention convention = new AuthorizationObservationConvention(); public ObservationReactiveAuthorizationManager(ObservationRegistry registry, ReactiveAuthorizationManager delegate) { @@ -70,15 +68,4 @@ public final class ObservationReactiveAuthorizationManager implements Reactiv }); } - /** - * Use the provided convention for reporting observation data - * @param convention The provided convention - * - * @since 6.1 - */ - public void setObservationConvention(ObservationConvention> convention) { - Assert.notNull(convention, "The observation convention cannot be null"); - this.convention = convention; - } - } diff --git a/core/src/main/java/org/springframework/security/authorization/method/Jsr250AuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/Jsr250AuthorizationManager.java index beb318ed15..a93c0d336d 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/Jsr250AuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/Jsr250AuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ package org.springframework.security.authorization.method; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.function.Supplier; @@ -31,7 +30,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.support.AopUtils; import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.lang.NonNull; -import org.springframework.security.authorization.AuthoritiesAuthorizationManager; +import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; @@ -58,23 +57,8 @@ public final class Jsr250AuthorizationManager implements AuthorizationManager> authoritiesAuthorizationManager = new AuthoritiesAuthorizationManager(); - private String rolePrefix = "ROLE_"; - /** - * Sets an {@link AuthorizationManager} that accepts a collection of authority - * strings. - * @param authoritiesAuthorizationManager the {@link AuthorizationManager} that - * accepts a collection of authority strings to use - * @since 6.2 - */ - public void setAuthoritiesAuthorizationManager( - AuthorizationManager> authoritiesAuthorizationManager) { - Assert.notNull(authoritiesAuthorizationManager, "authoritiesAuthorizationManager cannot be null"); - this.authoritiesAuthorizationManager = authoritiesAuthorizationManager; - } - /** * Sets the role prefix. Defaults to "ROLE_". * @param rolePrefix the role prefix to use @@ -111,9 +95,10 @@ public final class Jsr250AuthorizationManager implements AuthorizationManager new AuthorizationDecision(true); } - if (annotation instanceof RolesAllowed rolesAllowed) { - return (a, o) -> Jsr250AuthorizationManager.this.authoritiesAuthorizationManager.check(a, - getAllowedRolesWithPrefix(rolesAllowed)); + if (annotation instanceof RolesAllowed) { + RolesAllowed rolesAllowed = (RolesAllowed) annotation; + return AuthorityAuthorizationManager.hasAnyRole(Jsr250AuthorizationManager.this.rolePrefix, + rolesAllowed.value()); } return NULL_MANAGER; } @@ -160,14 +145,6 @@ public final class Jsr250AuthorizationManager implements AuthorizationManager getAllowedRolesWithPrefix(RolesAllowed rolesAllowed) { - Set roles = new HashSet<>(); - for (int i = 0; i < rolesAllowed.value().length; i++) { - roles.add(Jsr250AuthorizationManager.this.rolePrefix + rolesAllowed.value()[i]); - } - return roles; - } - } } diff --git a/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java index dcfc8a8511..73ad02ce38 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,23 +17,17 @@ package org.springframework.security.authorization.method; import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.support.AopUtils; -import org.springframework.core.MethodClassKey; +import org.springframework.lang.NonNull; import org.springframework.security.access.annotation.Secured; -import org.springframework.security.authorization.AuthoritiesAuthorizationManager; +import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; -import org.springframework.util.Assert; /** * An {@link AuthorizationManager} which can determine if an {@link Authentication} may @@ -45,22 +39,7 @@ import org.springframework.util.Assert; */ public final class SecuredAuthorizationManager implements AuthorizationManager { - private AuthorizationManager> authoritiesAuthorizationManager = new AuthoritiesAuthorizationManager(); - - private final Map> cachedAuthorities = new ConcurrentHashMap<>(); - - /** - * Sets an {@link AuthorizationManager} that accepts a collection of authority - * strings. - * @param authoritiesAuthorizationManager the {@link AuthorizationManager} that - * accepts a collection of authority strings to use - * @since 6.1 - */ - public void setAuthoritiesAuthorizationManager( - AuthorizationManager> authoritiesAuthorizationManager) { - Assert.notNull(authoritiesAuthorizationManager, "authoritiesAuthorizationManager cannot be null"); - this.authoritiesAuthorizationManager = authoritiesAuthorizationManager; - } + private final SecuredAuthorizationManagerRegistry registry = new SecuredAuthorizationManagerRegistry(); /** * Determine if an {@link Authentication} has access to a method by evaluating the @@ -72,28 +51,26 @@ public final class SecuredAuthorizationManager implements AuthorizationManager authentication, MethodInvocation mi) { - Set authorities = getAuthorities(mi); - return authorities.isEmpty() ? null : this.authoritiesAuthorizationManager.check(authentication, authorities); + AuthorizationManager delegate = this.registry.getManager(mi); + return delegate.check(authentication, mi); } - private Set getAuthorities(MethodInvocation methodInvocation) { - Method method = methodInvocation.getMethod(); - Object target = methodInvocation.getThis(); - Class targetClass = (target != null) ? target.getClass() : null; - MethodClassKey cacheKey = new MethodClassKey(method, targetClass); - return this.cachedAuthorities.computeIfAbsent(cacheKey, (k) -> resolveAuthorities(method, targetClass)); - } + private static final class SecuredAuthorizationManagerRegistry extends AbstractAuthorizationManagerRegistry { - private Set resolveAuthorities(Method method, Class targetClass) { - Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); - Secured secured = findSecuredAnnotation(specificMethod); - return (secured != null) ? Set.of(secured.value()) : Collections.emptySet(); - } + @NonNull + @Override + AuthorizationManager resolveManager(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + Secured secured = findSecuredAnnotation(specificMethod); + return (secured != null) ? AuthorityAuthorizationManager.hasAnyAuthority(secured.value()) : NULL_MANAGER; + } + + private Secured findSecuredAnnotation(Method method) { + Secured secured = AuthorizationAnnotationUtils.findUniqueAnnotation(method, Secured.class); + return (secured != null) ? secured + : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), Secured.class); + } - private Secured findSecuredAnnotation(Method method) { - Secured secured = AuthorizationAnnotationUtils.findUniqueAnnotation(method, Secured.class); - return (secured != null) ? secured - : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), Secured.class); } } diff --git a/core/src/main/java/org/springframework/security/core/ComparableVersion.java b/core/src/main/java/org/springframework/security/core/ComparableVersion.java index a7477a4a5b..58e278754b 100644 --- a/core/src/main/java/org/springframework/security/core/ComparableVersion.java +++ b/core/src/main/java/org/springframework/security/core/ComparableVersion.java @@ -130,13 +130,23 @@ class ComparableVersion implements Comparable { return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 } - return switch (item.getType()) { - case INT_ITEM -> Integer.compare(value, ((IntItem) item).value); - case LONG_ITEM, BIGINTEGER_ITEM -> -1; - case STRING_ITEM -> 1; // 1.1 > 1-sp - case LIST_ITEM -> 1; // 1.1 > 1-1 - default -> throw new IllegalStateException("invalid item: " + item.getClass()); - }; + switch (item.getType()) { + case INT_ITEM: + int itemValue = ((IntItem) item).value; + return (value < itemValue) ? -1 : ((value == itemValue) ? 0 : 1); + case LONG_ITEM: + case BIGINTEGER_ITEM: + return -1; + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } } @Override @@ -194,14 +204,24 @@ class ComparableVersion implements Comparable { return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1 } - return switch (item.getType()) { - case INT_ITEM -> 1; - case LONG_ITEM -> Long.compare(value, ((LongItem) item).value); - case BIGINTEGER_ITEM -> -1; - case STRING_ITEM -> 1; // 1.1 > 1-sp - case LIST_ITEM -> 1; // 1.1 > 1-1 - default -> throw new IllegalStateException("invalid item: " + item.getClass()); - }; + switch (item.getType()) { + case INT_ITEM: + return 1; + case LONG_ITEM: + long itemValue = ((LongItem) item).value; + return (value < itemValue) ? -1 : ((value == itemValue) ? 0 : 1); + case BIGINTEGER_ITEM: + return -1; + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } } @Override @@ -258,13 +278,23 @@ class ComparableVersion implements Comparable { return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 } - return switch (item.getType()) { - case INT_ITEM, LONG_ITEM -> 1; - case BIGINTEGER_ITEM -> value.compareTo(((BigIntegerItem) item).value); - case STRING_ITEM -> 1; // 1.1 > 1-sp - case LIST_ITEM -> 1; // 1.1 > 1-1 - default -> throw new IllegalStateException("invalid item: " + item.getClass()); - }; + switch (item.getType()) { + case INT_ITEM: + case LONG_ITEM: + return 1; + + case BIGINTEGER_ITEM: + return value.compareTo(((BigIntegerItem) item).value); + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } } @Override @@ -321,12 +351,18 @@ class ComparableVersion implements Comparable { StringItem(String value, boolean followedByDigit) { if (followedByDigit && value.length() == 1) { // a1 = alpha-1, b1 = beta-1, m1 = milestone-1 - value = switch (value.charAt(0)) { - case 'a' -> "alpha"; - case 'b' -> "beta"; - case 'm' -> "milestone"; - default -> value; - }; + switch (value.charAt(0)) { + case 'a': + value = "alpha"; + break; + case 'b': + value = "beta"; + break; + case 'm': + value = "milestone"; + break; + default: + } } this.value = ALIASES.getProperty(value, value); } @@ -366,13 +402,21 @@ class ComparableVersion implements Comparable { // 1-rc < 1, 1-ga > 1 return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX); } - return switch (item.getType()) { - case INT_ITEM, LONG_ITEM, BIGINTEGER_ITEM -> -1; // 1.any < 1.1 ? - case STRING_ITEM -> - comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value)); - case LIST_ITEM -> -1; // 1.any < 1-1 - default -> throw new IllegalStateException("invalid item: " + item.getClass()); - }; + switch (item.getType()) { + case INT_ITEM: + case LONG_ITEM: + case BIGINTEGER_ITEM: + return -1; // 1.any < 1.1 ? + + case STRING_ITEM: + return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value)); + + case LIST_ITEM: + return -1; // 1.any < 1-1 + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } } @Override @@ -440,12 +484,19 @@ class ComparableVersion implements Comparable { Item first = get(0); return first.compareTo(null); } - return switch (item.getType()) { - case INT_ITEM, LONG_ITEM, BIGINTEGER_ITEM -> -1; // 1-1 < 1.0.x - case STRING_ITEM -> 1; // 1-1 > 1-sp - case LIST_ITEM -> { + switch (item.getType()) { + case INT_ITEM: + case LONG_ITEM: + case BIGINTEGER_ITEM: + return -1; // 1-1 < 1.0.x + + case STRING_ITEM: + return 1; // 1-1 > 1-sp + + case LIST_ITEM: Iterator left = iterator(); Iterator right = ((ListItem) item).iterator(); + while (left.hasNext() || right.hasNext()) { Item l = left.hasNext() ? left.next() : null; Item r = right.hasNext() ? right.next() : null; @@ -454,13 +505,15 @@ class ComparableVersion implements Comparable { int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r); if (result != 0) { - yield result; + return result; } } - yield 0; - } - default -> throw new IllegalStateException("invalid item: " + item.getClass()); - }; + + return 0; + + default: + throw new IllegalStateException("invalid item: " + item.getClass()); + } } @Override diff --git a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java index 3d62aee5ad..8fee6191c0 100644 --- a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java +++ b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java @@ -43,7 +43,7 @@ public final class SpringSecurityCoreVersion { * N.B. Classes are not intended to be serializable between different versions. See * SEC-1709 for why we still need a serial version. */ - public static final long SERIAL_VERSION_UID = 620L; + public static final long SERIAL_VERSION_UID = 600L; static final String MIN_SPRING_VERSION = getSpringVersion(); diff --git a/core/src/main/java/org/springframework/security/core/authority/AuthorityUtils.java b/core/src/main/java/org/springframework/security/core/authority/AuthorityUtils.java index 03a3dcd21a..babadbca1d 100644 --- a/core/src/main/java/org/springframework/security/core/authority/AuthorityUtils.java +++ b/core/src/main/java/org/springframework/security/core/authority/AuthorityUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * 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. @@ -33,7 +33,6 @@ import org.springframework.util.StringUtils; * Mainly intended for internal use. * * @author Luke Taylor - * @author Evgeniy Cheban */ public final class AuthorityUtils { @@ -79,18 +78,4 @@ public final class AuthorityUtils { return grantedAuthorities; } - /** - * Converts authorities into a List of GrantedAuthority objects. - * @param authorities the authorities to convert - * @return a List of GrantedAuthority objects - * @since 6.1 - */ - public static List createAuthorityList(Collection authorities) { - List grantedAuthorities = new ArrayList<>(authorities.size()); - for (String authority : authorities) { - grantedAuthorities.add(new SimpleGrantedAuthority(authority)); - } - return grantedAuthorities; - } - } diff --git a/core/src/main/java/org/springframework/security/core/authority/SimpleGrantedAuthority.java b/core/src/main/java/org/springframework/security/core/authority/SimpleGrantedAuthority.java index 54b889e617..71719d4801 100644 --- a/core/src/main/java/org/springframework/security/core/authority/SimpleGrantedAuthority.java +++ b/core/src/main/java/org/springframework/security/core/authority/SimpleGrantedAuthority.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -50,8 +50,8 @@ public final class SimpleGrantedAuthority implements GrantedAuthority { if (this == obj) { return true; } - if (obj instanceof SimpleGrantedAuthority sga) { - return this.role.equals(sga.getAuthority()); + if (obj instanceof SimpleGrantedAuthority) { + return this.role.equals(((SimpleGrantedAuthority) obj).role); } return false; } diff --git a/core/src/main/java/org/springframework/security/core/authority/mapping/MapBasedAttributes2GrantedAuthoritiesMapper.java b/core/src/main/java/org/springframework/security/core/authority/mapping/MapBasedAttributes2GrantedAuthoritiesMapper.java index 1bca8eb7e3..263973f5d2 100755 --- a/core/src/main/java/org/springframework/security/core/authority/mapping/MapBasedAttributes2GrantedAuthoritiesMapper.java +++ b/core/src/main/java/org/springframework/security/core/authority/mapping/MapBasedAttributes2GrantedAuthoritiesMapper.java @@ -117,6 +117,7 @@ public class MapBasedAttributes2GrantedAuthoritiesMapper * Convert the given value to a collection of Granted Authorities, adding the result * to the given result collection. * @param value The value to convert to a GrantedAuthority Collection + * @return Collection containing the GrantedAuthority Collection */ private void addGrantedAuthorityCollection(Collection result, Object value) { if (value == null) { diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java index 6b23733667..aaf7def3c2 100644 --- a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java @@ -48,7 +48,7 @@ public interface SecurityContextHolderStrategy { * @since 5.8 */ default Supplier getDeferredContext() { - return this::getContext; + return () -> getContext(); } /** diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextImpl.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextImpl.java index c83bba8741..734373df57 100644 --- a/core/src/main/java/org/springframework/security/core/context/SecurityContextImpl.java +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextImpl.java @@ -42,7 +42,8 @@ public class SecurityContextImpl implements SecurityContext { @Override public boolean equals(Object obj) { - if (obj instanceof SecurityContextImpl other) { + if (obj instanceof SecurityContextImpl) { + SecurityContextImpl other = (SecurityContextImpl) obj; if ((this.getAuthentication() == null) && (other.getAuthentication() == null)) { return true; } diff --git a/core/src/main/java/org/springframework/security/core/parameters/AnnotationParameterNameDiscoverer.java b/core/src/main/java/org/springframework/security/core/parameters/AnnotationParameterNameDiscoverer.java index 8f1fbc852b..18847e0a28 100644 --- a/core/src/main/java/org/springframework/security/core/parameters/AnnotationParameterNameDiscoverer.java +++ b/core/src/main/java/org/springframework/security/core/parameters/AnnotationParameterNameDiscoverer.java @@ -180,6 +180,7 @@ public class AnnotationParameterNameDiscoverer implements ParameterNameDiscovere /** * Gets the {@link Annotation}s at a specified index * @param t + * @param index * @return */ Annotation[][] findParameterAnnotations(T t); diff --git a/core/src/main/java/org/springframework/security/core/parameters/DefaultSecurityParameterNameDiscoverer.java b/core/src/main/java/org/springframework/security/core/parameters/DefaultSecurityParameterNameDiscoverer.java index 8c2792eaa8..7a7b339917 100644 --- a/core/src/main/java/org/springframework/security/core/parameters/DefaultSecurityParameterNameDiscoverer.java +++ b/core/src/main/java/org/springframework/security/core/parameters/DefaultSecurityParameterNameDiscoverer.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Set; import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.PrioritizedParameterNameDiscoverer; import org.springframework.util.Assert; @@ -35,6 +36,11 @@ import org.springframework.util.ClassUtils; *

  • Will use an instance of {@link AnnotationParameterNameDiscoverer} with {@link P} as * a valid annotation. If, Spring Data is on the classpath will also add Param annotation. *
  • + *
  • If Spring 4 is on the classpath, then DefaultParameterNameDiscoverer is added. This + * attempts to use JDK 8 information first and falls back to + * {@link LocalVariableTableParameterNameDiscoverer}.
  • + *
  • If Spring 4 is not on the classpath, then + * {@link LocalVariableTableParameterNameDiscoverer} is added directly.
  • * * * @author Rob Winch diff --git a/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java b/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java index a390931541..4aa6c2af75 100644 --- a/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java +++ b/core/src/main/java/org/springframework/security/core/session/SessionRegistryImpl.java @@ -100,11 +100,13 @@ public class SessionRegistryImpl implements SessionRegistry, ApplicationListener @Override public void onApplicationEvent(AbstractSessionEvent event) { - if (event instanceof SessionDestroyedEvent sessionDestroyedEvent) { + if (event instanceof SessionDestroyedEvent) { + SessionDestroyedEvent sessionDestroyedEvent = (SessionDestroyedEvent) event; String sessionId = sessionDestroyedEvent.getId(); removeSessionInformation(sessionId); } - else if (event instanceof SessionIdChangedEvent sessionIdChangedEvent) { + else if (event instanceof SessionIdChangedEvent) { + SessionIdChangedEvent sessionIdChangedEvent = (SessionIdChangedEvent) event; String oldSessionId = sessionIdChangedEvent.getOldSessionId(); if (this.sessionIds.containsKey(oldSessionId)) { Object principal = this.sessionIds.get(oldSessionId).getPrincipal(); diff --git a/core/src/main/java/org/springframework/security/core/token/DefaultToken.java b/core/src/main/java/org/springframework/security/core/token/DefaultToken.java index 852dcc52ed..aaa89ffb6d 100644 --- a/core/src/main/java/org/springframework/security/core/token/DefaultToken.java +++ b/core/src/main/java/org/springframework/security/core/token/DefaultToken.java @@ -59,7 +59,8 @@ public class DefaultToken implements Token { @Override public boolean equals(Object obj) { - if (obj instanceof DefaultToken rhs) { + if (obj != null && obj instanceof DefaultToken) { + DefaultToken rhs = (DefaultToken) obj; return this.key.equals(rhs.key) && this.keyCreationTime == rhs.keyCreationTime && this.extendedInformation.equals(rhs.extendedInformation); } @@ -70,7 +71,7 @@ public class DefaultToken implements Token { public int hashCode() { int code = 979; code = code * this.key.hashCode(); - code = code * Long.valueOf(this.keyCreationTime).hashCode(); + code = code * new Long(this.keyCreationTime).hashCode(); code = code * this.extendedInformation.hashCode(); return code; } diff --git a/core/src/main/java/org/springframework/security/core/token/KeyBasedPersistenceTokenService.java b/core/src/main/java/org/springframework/security/core/token/KeyBasedPersistenceTokenService.java index d2ba33c3b3..8ac12a5348 100644 --- a/core/src/main/java/org/springframework/security/core/token/KeyBasedPersistenceTokenService.java +++ b/core/src/main/java/org/springframework/security/core/token/KeyBasedPersistenceTokenService.java @@ -142,7 +142,7 @@ public class KeyBasedPersistenceTokenService implements TokenService, Initializi } private String computeServerSecretApplicableAt(long time) { - return this.serverSecret + ":" + Long.valueOf(time % this.serverInteger).intValue(); + return this.serverSecret + ":" + new Long(time % this.serverInteger).intValue(); } /** diff --git a/core/src/main/java/org/springframework/security/core/userdetails/User.java b/core/src/main/java/org/springframework/security/core/userdetails/User.java index 461e2b478d..86d57704df 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/User.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/User.java @@ -179,8 +179,8 @@ public class User implements UserDetails, CredentialsContainer { */ @Override public boolean equals(Object obj) { - if (obj instanceof User user) { - return this.username.equals(user.getUsername()); + if (obj instanceof User) { + return this.username.equals(((User) obj).username); } return false; } @@ -201,14 +201,14 @@ public class User implements UserDetails, CredentialsContainer { sb.append("Password=[PROTECTED], "); sb.append("Enabled=").append(this.enabled).append(", "); sb.append("AccountNonExpired=").append(this.accountNonExpired).append(", "); - sb.append("CredentialsNonExpired=").append(this.credentialsNonExpired).append(", "); + sb.append("credentialsNonExpired=").append(this.credentialsNonExpired).append(", "); sb.append("AccountNonLocked=").append(this.accountNonLocked).append(", "); sb.append("Granted Authorities=").append(this.authorities).append("]"); return sb.toString(); } /** - * Creates a UserBuilder with a specified username + * Creates a UserBuilder with a specified user name * @param username the username to use * @return the UserBuilder */ @@ -329,7 +329,7 @@ public class User implements UserDetails, CredentialsContainer { private String password; - private List authorities = new ArrayList<>(); + private List authorities; private boolean accountExpired; @@ -427,7 +427,6 @@ public class User implements UserDetails, CredentialsContainer { * @see #roles(String...) */ public UserBuilder authorities(GrantedAuthority... authorities) { - Assert.notNull(authorities, "authorities cannot be null"); return authorities(Arrays.asList(authorities)); } @@ -440,7 +439,6 @@ public class User implements UserDetails, CredentialsContainer { * @see #roles(String...) */ public UserBuilder authorities(Collection authorities) { - Assert.notNull(authorities, "authorities cannot be null"); this.authorities = new ArrayList<>(authorities); return this; } @@ -454,7 +452,6 @@ public class User implements UserDetails, CredentialsContainer { * @see #roles(String...) */ public UserBuilder authorities(String... authorities) { - Assert.notNull(authorities, "authorities cannot be null"); return authorities(AuthorityUtils.createAuthorityList(authorities)); } diff --git a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java index 955355a4f9..abccf9ac50 100644 --- a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java +++ b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java @@ -127,7 +127,7 @@ public final class SecurityJackson2Modules { Class securityModule = (Class) ClassUtils.forName(className, loader); if (securityModule != null) { logger.debug(LogMessage.format("Loaded module %s, now registering", className)); - return securityModule.getConstructor().newInstance(); + return securityModule.newInstance(); } } catch (Exception ex) { diff --git a/core/src/main/java/org/springframework/security/jackson2/UnmodifiableListDeserializer.java b/core/src/main/java/org/springframework/security/jackson2/UnmodifiableListDeserializer.java index 8dc8ef286a..fd86dadccc 100644 --- a/core/src/main/java/org/springframework/security/jackson2/UnmodifiableListDeserializer.java +++ b/core/src/main/java/org/springframework/security/jackson2/UnmodifiableListDeserializer.java @@ -44,7 +44,8 @@ class UnmodifiableListDeserializer extends JsonDeserializer { JsonNode node = mapper.readTree(jp); List result = new ArrayList<>(); if (node != null) { - if (node instanceof ArrayNode arrayNode) { + if (node instanceof ArrayNode) { + ArrayNode arrayNode = (ArrayNode) node; for (JsonNode elementNode : arrayNode) { result.add(mapper.readValue(elementNode.traverse(mapper), Object.class)); } diff --git a/core/src/main/java/org/springframework/security/jackson2/UnmodifiableSetDeserializer.java b/core/src/main/java/org/springframework/security/jackson2/UnmodifiableSetDeserializer.java index 74e19fa8bb..c26d6921b5 100644 --- a/core/src/main/java/org/springframework/security/jackson2/UnmodifiableSetDeserializer.java +++ b/core/src/main/java/org/springframework/security/jackson2/UnmodifiableSetDeserializer.java @@ -44,7 +44,8 @@ class UnmodifiableSetDeserializer extends JsonDeserializer { JsonNode node = mapper.readTree(jp); Set resultSet = new HashSet<>(); if (node != null) { - if (node instanceof ArrayNode arrayNode) { + if (node instanceof ArrayNode) { + ArrayNode arrayNode = (ArrayNode) node; for (JsonNode elementNode : arrayNode) { resultSet.add(mapper.readValue(elementNode.traverse(mapper), Object.class)); } diff --git a/core/src/main/java/org/springframework/security/util/MethodInvocationUtils.java b/core/src/main/java/org/springframework/security/util/MethodInvocationUtils.java index 9c407bfd40..26ade0d092 100644 --- a/core/src/main/java/org/springframework/security/util/MethodInvocationUtils.java +++ b/core/src/main/java/org/springframework/security/util/MethodInvocationUtils.java @@ -60,7 +60,8 @@ public final class MethodInvocationUtils { // Determine the type that declares the requested method, // taking into account proxies Class target = AopUtils.getTargetClass(object); - if (object instanceof Advised a) { + if (object instanceof Advised) { + Advised a = (Advised) object; if (!a.isProxyTargetClass()) { Class[] possibleInterfaces = a.getProxiedInterfaces(); for (Class possibleInterface : possibleInterfaces) { diff --git a/core/src/test/java/org/springframework/security/DelegatingSecurityContextTestUtils.java b/core/src/test/java/org/springframework/security/DelegatingSecurityContextTestUtils.java deleted file mode 100644 index 0c05cf588a..0000000000 --- a/core/src/test/java/org/springframework/security/DelegatingSecurityContextTestUtils.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2002-2023 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; - -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; - -import org.springframework.scheduling.TaskScheduler; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -/** - * @author Steve Riesenberg - */ -public final class DelegatingSecurityContextTestUtils { - - private DelegatingSecurityContextTestUtils() { - } - - public static SecurityContext runAndReturn(ThreadFactory threadFactory, - Function factory, BiConsumer fn) throws Exception { - CountDownLatch countDownLatch = new CountDownLatch(1); - AtomicReference result = new AtomicReference<>(); - ScheduledExecutorService delegate = Executors.newSingleThreadScheduledExecutor(threadFactory); - try { - T executor = factory.apply(delegate); - Runnable task = () -> { - result.set(SecurityContextHolder.getContext()); - countDownLatch.countDown(); - }; - fn.accept(executor, task); - countDownLatch.await(); - - return result.get(); - } - finally { - delegate.shutdown(); - } - } - - public static SecurityContext runAndReturn(ThreadFactory threadFactory, - Function factory, BiFunction> fn) - throws Exception { - CountDownLatch countDownLatch = new CountDownLatch(1); - AtomicReference result = new AtomicReference<>(); - ScheduledExecutorService delegate = Executors.newSingleThreadScheduledExecutor(threadFactory); - try { - T taskScheduler = factory.apply(delegate); - Runnable task = () -> { - result.set(SecurityContextHolder.getContext()); - countDownLatch.countDown(); - }; - ScheduledFuture future = fn.apply(taskScheduler, task); - countDownLatch.await(); - future.cancel(false); - - return result.get(); - } - finally { - delegate.shutdown(); - } - } - - public static SecurityContext callAndReturn(ThreadFactory threadFactory, - Function factory, - BiFunction, Future> fn) throws Exception { - ScheduledExecutorService delegate = Executors.newSingleThreadScheduledExecutor(threadFactory); - try { - T executor = factory.apply(delegate); - Callable task = SecurityContextHolder::getContext; - return fn.apply(executor, task).get(); - } - finally { - delegate.shutdown(); - } - } - -} diff --git a/core/src/test/java/org/springframework/security/access/intercept/RunAsImplAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/access/intercept/RunAsImplAuthenticationProviderTests.java index 91a616415b..18e79ca1eb 100644 --- a/core/src/test/java/org/springframework/security/access/intercept/RunAsImplAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/access/intercept/RunAsImplAuthenticationProviderTests.java @@ -16,6 +16,7 @@ package org.springframework.security.access.intercept; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.security.authentication.BadCredentialsException; @@ -49,7 +50,7 @@ public class RunAsImplAuthenticationProviderTests { RunAsImplAuthenticationProvider provider = new RunAsImplAuthenticationProvider(); provider.setKey("my_password"); Authentication result = provider.authenticate(token); - assertThat(result instanceof RunAsUserToken).as("Should have returned RunAsUserToken").isTrue(); + Assertions.assertTrue(result instanceof RunAsUserToken, "Should have returned RunAsUserToken"); RunAsUserToken resultCast = (RunAsUserToken) result; assertThat(resultCast.getKeyHash()).isEqualTo("my_password".hashCode()); } diff --git a/core/src/test/java/org/springframework/security/access/intercept/RunAsManagerImplTests.java b/core/src/test/java/org/springframework/security/access/intercept/RunAsManagerImplTests.java index 16dc6a5c77..cd877b09cd 100644 --- a/core/src/test/java/org/springframework/security/access/intercept/RunAsManagerImplTests.java +++ b/core/src/test/java/org/springframework/security/access/intercept/RunAsManagerImplTests.java @@ -66,9 +66,9 @@ public class RunAsManagerImplTests { assertThat(result.getPrincipal()).isEqualTo(inputToken.getPrincipal()); assertThat(result.getCredentials()).isEqualTo(inputToken.getCredentials()); Set authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); - assertThat(authorities).contains("FOOBAR_RUN_AS_SOMETHING"); - assertThat(authorities).contains("ONE"); - assertThat(authorities).contains("TWO"); + assertThat(authorities.contains("FOOBAR_RUN_AS_SOMETHING")).isTrue(); + assertThat(authorities.contains("ONE")).isTrue(); + assertThat(authorities.contains("TWO")).isTrue(); RunAsUserToken resultCast = (RunAsUserToken) result; assertThat(resultCast.getKeyHash()).isEqualTo("my_password".hashCode()); } @@ -87,9 +87,9 @@ public class RunAsManagerImplTests { assertThat(result.getPrincipal()).isEqualTo(inputToken.getPrincipal()); assertThat(result.getCredentials()).isEqualTo(inputToken.getCredentials()); Set authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); - assertThat(authorities).contains("ROLE_RUN_AS_SOMETHING"); - assertThat(authorities).contains("ROLE_ONE"); - assertThat(authorities).contains("ROLE_TWO"); + assertThat(authorities.contains("ROLE_RUN_AS_SOMETHING")).isTrue(); + assertThat(authorities.contains("ROLE_ONE")).isTrue(); + assertThat(authorities.contains("ROLE_TWO")).isTrue(); RunAsUserToken resultCast = (RunAsUserToken) result; assertThat(resultCast.getKeyHash()).isEqualTo("my_password".hashCode()); } diff --git a/core/src/test/java/org/springframework/security/access/method/DelegatingMethodSecurityMetadataSourceTests.java b/core/src/test/java/org/springframework/security/access/method/DelegatingMethodSecurityMetadataSourceTests.java index 3989732410..3d388d2f48 100644 --- a/core/src/test/java/org/springframework/security/access/method/DelegatingMethodSecurityMetadataSourceTests.java +++ b/core/src/test/java/org/springframework/security/access/method/DelegatingMethodSecurityMetadataSourceTests.java @@ -50,7 +50,7 @@ public class DelegatingMethodSecurityMetadataSourceTests { sources.add(delegate); this.mds = new DelegatingMethodSecurityMetadataSource(sources); assertThat(this.mds.getMethodSecurityMetadataSources()).isSameAs(sources); - assertThat(this.mds.getAllConfigAttributes()).isEmpty(); + assertThat(this.mds.getAllConfigAttributes().isEmpty()).isTrue(); MethodInvocation mi = new SimpleMethodInvocation(null, String.class.getMethod("toString")); assertThat(this.mds.getAttributes(mi)).isEqualTo(Collections.emptyList()); // Exercise the cached case @@ -68,7 +68,7 @@ public class DelegatingMethodSecurityMetadataSourceTests { sources.add(delegate); this.mds = new DelegatingMethodSecurityMetadataSource(sources); assertThat(this.mds.getMethodSecurityMetadataSources()).isSameAs(sources); - assertThat(this.mds.getAllConfigAttributes()).isEmpty(); + assertThat(this.mds.getAllConfigAttributes().isEmpty()).isTrue(); MethodInvocation mi = new SimpleMethodInvocation("", toString); assertThat(this.mds.getAttributes(mi)).isSameAs(attributes); // Exercise the cached case diff --git a/core/src/test/java/org/springframework/security/access/vote/DenyAgainVoter.java b/core/src/test/java/org/springframework/security/access/vote/DenyAgainVoter.java index 9fcfc7fb3a..4d59a0173b 100644 --- a/core/src/test/java/org/springframework/security/access/vote/DenyAgainVoter.java +++ b/core/src/test/java/org/springframework/security/access/vote/DenyAgainVoter.java @@ -17,6 +17,7 @@ package org.springframework.security.access.vote; import java.util.Collection; +import java.util.Iterator; import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.ConfigAttribute; @@ -46,7 +47,9 @@ public class DenyAgainVoter implements AccessDecisionVoter { @Override public int vote(Authentication authentication, Object object, Collection attributes) { - for (ConfigAttribute attribute : attributes) { + Iterator iter = attributes.iterator(); + while (iter.hasNext()) { + ConfigAttribute attribute = iter.next(); if (this.supports(attribute)) { return ACCESS_DENIED; } diff --git a/core/src/test/java/org/springframework/security/access/vote/DenyVoter.java b/core/src/test/java/org/springframework/security/access/vote/DenyVoter.java index 6408c9a570..b20964b020 100644 --- a/core/src/test/java/org/springframework/security/access/vote/DenyVoter.java +++ b/core/src/test/java/org/springframework/security/access/vote/DenyVoter.java @@ -17,6 +17,7 @@ package org.springframework.security.access.vote; import java.util.Collection; +import java.util.Iterator; import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.ConfigAttribute; @@ -48,7 +49,9 @@ public class DenyVoter implements AccessDecisionVoter { @Override public int vote(Authentication authentication, Object object, Collection attributes) { - for (ConfigAttribute attribute : attributes) { + Iterator iter = attributes.iterator(); + while (iter.hasNext()) { + ConfigAttribute attribute = iter.next(); if (this.supports(attribute)) { return ACCESS_DENIED; } diff --git a/core/src/test/java/org/springframework/security/authentication/ObservationAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/ObservationAuthenticationManagerTests.java index f7ded50f7b..4583d61746 100644 --- a/core/src/test/java/org/springframework/security/authentication/ObservationAuthenticationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/ObservationAuthenticationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -93,10 +93,4 @@ public class ObservationAuthenticationManagerTests { assertThat(context.getAuthenticationResult()).isNull(); } - @Test - void setObservationConventionWhenNullThenException() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> this.tested.setObservationConvention(null)); - } - } diff --git a/core/src/test/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManagerTests.java index c96056225a..fb4c50e622 100644 --- a/core/src/test/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/ObservationReactiveAuthenticationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -96,10 +96,4 @@ public class ObservationReactiveAuthenticationManagerTests { assertThat(context.getAuthenticationResult()).isNull(); } - @Test - void setObservationConventionWhenNullThenException() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> this.tested.setObservationConvention(null)); - } - } diff --git a/core/src/test/java/org/springframework/security/authentication/TestAuthentication.java b/core/src/test/java/org/springframework/security/authentication/TestAuthentication.java index a0faebee71..cdeb4ba1d8 100644 --- a/core/src/test/java/org/springframework/security/authentication/TestAuthentication.java +++ b/core/src/test/java/org/springframework/security/authentication/TestAuthentication.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,23 +17,15 @@ package org.springframework.security.authentication; import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.UserDetails; /** * @author Rob Winch - * @author Evgeniy Cheban * @since 5.0 */ public class TestAuthentication extends PasswordEncodedUser { - private static final Authentication ANONYMOUS = new AnonymousAuthenticationToken("key", "anonymous", - AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); - - private static final RememberMeAuthenticationToken REMEMBER_ME = new RememberMeAuthenticationToken("key", "user", - AuthorityUtils.createAuthorityList("ROLE_USER")); - public static Authentication authenticatedAdmin() { return autheticated(admin()); } @@ -46,12 +38,4 @@ public class TestAuthentication extends PasswordEncodedUser { return UsernamePasswordAuthenticationToken.authenticated(user, null, user.getAuthorities()); } - public static Authentication anonymousUser() { - return ANONYMOUS; - } - - public static Authentication rememberMeUser() { - return REMEMBER_ME; - } - } diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java index 04cf7b85da..ea9bab5653 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/DefaultJaasAuthenticationProviderTests.java @@ -222,13 +222,16 @@ public class DefaultJaasAuthenticationProviderTests { public void javadocExample() { String resName = "/" + getClass().getName().replace('.', '/') + ".xml"; ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(resName); - try (context) { - context.registerShutdownHook(); + context.registerShutdownHook(); + try { this.provider = context.getBean(DefaultJaasAuthenticationProvider.class); Authentication auth = this.provider.authenticate(this.token); assertThat(auth.isAuthenticated()).isEqualTo(true); assertThat(auth.getPrincipal()).isEqualTo(this.token.getPrincipal()); } + finally { + context.close(); + } } private void verifyFailedLogin() { diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java index 351612f4ce..57c751f3cb 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationProviderTests.java @@ -174,7 +174,8 @@ public class JaasAuthenticationProviderTests { assertThat(set.contains("ROLE_TEST2")).withFailMessage("GrantedAuthorities should contain ROLE_TEST2").isTrue(); boolean foundit = false; for (GrantedAuthority a : list) { - if (a instanceof JaasGrantedAuthority grant) { + if (a instanceof JaasGrantedAuthority) { + JaasGrantedAuthority grant = (JaasGrantedAuthority) a; assertThat(grant.getPrincipal()).withFailMessage("Principal was null on JaasGrantedAuthority") .isNotNull(); foundit = true; diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/TestCallbackHandler.java b/core/src/test/java/org/springframework/security/authentication/jaas/TestCallbackHandler.java index 0084c95c8d..645b684709 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/TestCallbackHandler.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/TestCallbackHandler.java @@ -30,7 +30,8 @@ public class TestCallbackHandler implements JaasAuthenticationCallbackHandler { @Override public void handle(Callback callback, Authentication auth) { - if (callback instanceof TextInputCallback tic) { + if (callback instanceof TextInputCallback) { + TextInputCallback tic = (TextInputCallback) callback; tic.setText(auth.getPrincipal().toString()); } } diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/memory/InMemoryConfigurationTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/memory/InMemoryConfigurationTests.java index 7b7c6d0f35..0bb7b6896a 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/memory/InMemoryConfigurationTests.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/memory/InMemoryConfigurationTests.java @@ -89,7 +89,7 @@ public class InMemoryConfigurationTests { public void mappedNonnullDefault() { InMemoryConfiguration configuration = new InMemoryConfiguration(this.mappedEntries, this.defaultEntries); assertThat(this.defaultEntries).isEqualTo(configuration.getAppConfigurationEntry("missing")); - assertThat(this.mappedEntries).containsEntry("name", configuration.getAppConfigurationEntry("name")); + assertThat(this.mappedEntries.get("name")).isEqualTo(configuration.getAppConfigurationEntry("name")); } @Test diff --git a/core/src/test/java/org/springframework/security/authorization/AuthoritiesAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthoritiesAuthorizationManagerTests.java deleted file mode 100644 index 6474117ea4..0000000000 --- a/core/src/test/java/org/springframework/security/authorization/AuthoritiesAuthorizationManagerTests.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2002-2023 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.authorization; - -import java.util.Arrays; -import java.util.Collections; -import java.util.function.Supplier; - -import org.junit.jupiter.api.Test; - -import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; -import org.springframework.security.access.hierarchicalroles.RoleHierarchy; -import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.Authentication; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * Tests for {@link AuthoritiesAuthorizationManager}. - * - * @author Evgeniy Cheban - */ -class AuthoritiesAuthorizationManagerTests { - - @Test - void setRoleHierarchyWhenNullThenIllegalArgumentException() { - AuthoritiesAuthorizationManager manager = new AuthoritiesAuthorizationManager(); - assertThatIllegalArgumentException().isThrownBy(() -> manager.setRoleHierarchy(null)) - .withMessage("roleHierarchy cannot be null"); - } - - @Test - void setRoleHierarchyWhenNotNullThenVerifyRoleHierarchy() { - AuthoritiesAuthorizationManager manager = new AuthoritiesAuthorizationManager(); - RoleHierarchy roleHierarchy = new RoleHierarchyImpl(); - manager.setRoleHierarchy(roleHierarchy); - assertThat(manager).extracting("roleHierarchy").isEqualTo(roleHierarchy); - } - - @Test - void getRoleHierarchyWhenNotSetThenDefaultsToNullRoleHierarchy() { - AuthoritiesAuthorizationManager manager = new AuthoritiesAuthorizationManager(); - assertThat(manager).extracting("roleHierarchy").isInstanceOf(NullRoleHierarchy.class); - } - - @Test - void checkWhenUserHasAnyAuthorityThenGrantedDecision() { - AuthoritiesAuthorizationManager manager = new AuthoritiesAuthorizationManager(); - Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "USER"); - assertThat(manager.check(authentication, Arrays.asList("ADMIN", "USER")).isGranted()).isTrue(); - } - - @Test - void checkWhenUserHasNotAnyAuthorityThenDeniedDecision() { - AuthoritiesAuthorizationManager manager = new AuthoritiesAuthorizationManager(); - Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ANONYMOUS"); - assertThat(manager.check(authentication, Arrays.asList("ADMIN", "USER")).isGranted()).isFalse(); - } - - @Test - void checkWhenRoleHierarchySetThenGreaterRoleTakesPrecedence() { - AuthoritiesAuthorizationManager manager = new AuthoritiesAuthorizationManager(); - RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); - roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER"); - manager.setRoleHierarchy(roleHierarchy); - Supplier authentication = () -> new TestingAuthenticationToken("user", "password", - "ROLE_ADMIN"); - assertThat(manager.check(authentication, Collections.singleton("ROLE_USER")).isGranted()).isTrue(); - } - -} diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java index 73b3e04586..2913b169f3 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java @@ -245,13 +245,13 @@ public class AuthorityAuthorizationManagerTests { AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasRole("USER"); RoleHierarchy roleHierarchy = new RoleHierarchyImpl(); manager.setRoleHierarchy(roleHierarchy); - assertThat(manager).extracting("delegate").extracting("roleHierarchy").isEqualTo(roleHierarchy); + assertThat(manager).extracting("roleHierarchy").isEqualTo(roleHierarchy); } @Test public void getRoleHierarchyWhenNotSetThenDefaultsToNullRoleHierarchy() { AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasRole("USER"); - assertThat(manager).extracting("delegate").extracting("roleHierarchy").isInstanceOf(NullRoleHierarchy.class); + assertThat(manager).extracting("roleHierarchy").isInstanceOf(NullRoleHierarchy.class); } @Test diff --git a/core/src/test/java/org/springframework/security/authorization/ObservationAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/ObservationAuthorizationManagerTests.java index 1921f7dc1a..4040eed5a1 100644 --- a/core/src/test/java/org/springframework/security/authorization/ObservationAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/ObservationAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.security.authorization; -import java.util.Optional; import java.util.function.Supplier; import io.micrometer.observation.Observation; @@ -26,7 +25,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import org.springframework.context.MessageSource; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; @@ -34,7 +32,6 @@ import org.springframework.security.core.Authentication; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -88,20 +85,14 @@ public class ObservationAuthorizationManagerTests { @Test void verifyWhenErrorsThenObserves() { - MessageSource source = mock(MessageSource.class); - this.tested.setMessageSource(source); given(this.handler.supportsContext(any())).willReturn(true); given(this.authorizationManager.check(any(), any())).willReturn(this.deny); - given(source.getMessage(eq("AbstractAccessDecisionManager.accessDenied"), any(), any(), any())) - .willReturn("accessDenied"); assertThatExceptionOfType(AccessDeniedException.class) .isThrownBy(() -> this.tested.verify(this.token, this.object)); ArgumentCaptor captor = ArgumentCaptor.forClass(Observation.Context.class); verify(this.handler).onStart(captor.capture()); assertThat(captor.getValue().getName()).isEqualTo(AuthorizationObservationConvention.OBSERVATION_NAME); assertThat(captor.getValue().getError()).isInstanceOf(AccessDeniedException.class); - assertThat(Optional.ofNullable(captor.getValue().getError()).map(Throwable::getMessage).orElse("")) - .isEqualTo("accessDenied"); assertThat(captor.getValue()).isInstanceOf(AuthorizationObservationContext.class); AuthorizationObservationContext context = (AuthorizationObservationContext) captor.getValue(); assertThat(context.getAuthentication()).isNull(); @@ -127,10 +118,4 @@ public class ObservationAuthorizationManagerTests { assertThat(context.getDecision()).isEqualTo(this.grant); } - @Test - void setObservationConventionWhenNullThenException() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> this.tested.setObservationConvention(null)); - } - } diff --git a/core/src/test/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManagerTests.java index 34b0533a26..eb8f46a9bb 100644 --- a/core/src/test/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -117,10 +117,4 @@ public class ObservationReactiveAuthorizationManagerTests { assertThat(context.getDecision()).isEqualTo(this.grant); } - @Test - void setObservationConventionWhenNullThenException() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> this.tested.setObservationConvention(null)); - } - } diff --git a/core/src/test/java/org/springframework/security/authorization/SpringAuthorizationEventPublisherTests.java b/core/src/test/java/org/springframework/security/authorization/SpringAuthorizationEventPublisherTests.java index 466e484c74..cc56d8477e 100644 --- a/core/src/test/java/org/springframework/security/authorization/SpringAuthorizationEventPublisherTests.java +++ b/core/src/test/java/org/springframework/security/authorization/SpringAuthorizationEventPublisherTests.java @@ -38,7 +38,7 @@ import static org.mockito.Mockito.verifyNoInteractions; */ public class SpringAuthorizationEventPublisherTests { - Supplier authentication = TestAuthentication::authenticatedUser; + Supplier authentication = () -> TestAuthentication.authenticatedUser(); ApplicationEventPublisher applicationEventPublisher; diff --git a/core/src/test/java/org/springframework/security/authorization/method/Jsr250AuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/Jsr250AuthorizationManagerTests.java index 5a35b88489..017a56fe02 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/Jsr250AuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/Jsr250AuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,6 @@ package org.springframework.security.authorization.method; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Collection; -import java.util.Set; import java.util.function.Supplier; import jakarta.annotation.security.DenyAll; @@ -32,14 +30,11 @@ import org.springframework.security.access.intercept.method.MockMethodInvocation import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authorization.AuthorizationDecision; -import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link Jsr250AuthorizationManager}. @@ -68,27 +63,6 @@ public class Jsr250AuthorizationManagerTests { assertThat(manager).extracting("rolePrefix").isEqualTo("CUSTOM_"); } - @Test - public void setAuthoritiesAuthorizationManagerWhenNullThenException() { - Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); - assertThatIllegalArgumentException().isThrownBy(() -> manager.setAuthoritiesAuthorizationManager(null)) - .withMessage("authoritiesAuthorizationManager cannot be null"); - } - - @Test - public void setAuthoritiesAuthorizationManagerWhenNotNullThenVerifyUsage() throws Exception { - AuthorizationManager> authoritiesAuthorizationManager = mock(AuthorizationManager.class); - Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); - manager.setAuthoritiesAuthorizationManager(authoritiesAuthorizationManager); - MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(), - ClassLevelAnnotations.class, "rolesAllowedAdmin"); - Supplier authentication = () -> new TestingAuthenticationToken("user", "password", - "ROLE_ADMIN"); - AuthorizationDecision decision = manager.check(authentication, methodInvocation); - assertThat(decision).isNull(); - verify(authoritiesAuthorizationManager).check(authentication, Set.of("ROLE_ADMIN")); - } - @Test public void checkDoSomethingWhenNoJsr250AnnotationsThenNullDecision() throws Exception { MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, @@ -149,7 +123,7 @@ public class Jsr250AuthorizationManagerTests { } @Test - public void checkMultipleMethodAnnotationsWhenInvokedThenAnnotationConfigurationException() throws Exception { + public void checkMultipleAnnotationsWhenInvokedThenAnnotationConfigurationException() throws Exception { Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ANONYMOUS"); MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, @@ -159,16 +133,6 @@ public class Jsr250AuthorizationManagerTests { .isThrownBy(() -> manager.check(authentication, methodInvocation)); } - @Test - public void checkMultipleClassAnnotationsWhenInvokedThenAnnotationConfigurationException() throws Exception { - Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER"); - MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelIllegalAnnotations(), - ClassLevelIllegalAnnotations.class, "inheritedAnnotations"); - Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager(); - assertThatExceptionOfType(AnnotationConfigurationException.class) - .isThrownBy(() -> manager.check(authentication, methodInvocation)); - } - @Test public void checkRequiresAdminWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception { Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER"); @@ -283,15 +247,6 @@ public class Jsr250AuthorizationManagerTests { } - @MyIllegalRolesAllowed - public static class ClassLevelIllegalAnnotations { - - public void inheritedAnnotations() { - - } - - } - public interface InterfaceAnnotationsOne { @RolesAllowed("ADMIN") @@ -319,11 +274,4 @@ public class Jsr250AuthorizationManagerTests { } - @DenyAll - @RolesAllowed("USER") - @Retention(RetentionPolicy.RUNTIME) - public @interface MyIllegalRolesAllowed { - - } - } diff --git a/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java index 5d8651df9e..f4049be87f 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,6 @@ package org.springframework.security.authorization.method; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Collection; -import java.util.Set; import java.util.function.Supplier; import org.junit.jupiter.api.Test; @@ -31,14 +29,10 @@ import org.springframework.security.access.intercept.method.MockMethodInvocation import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authorization.AuthorizationDecision; -import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link SecuredAuthorizationManager}. @@ -47,26 +41,6 @@ import static org.mockito.Mockito.verify; */ public class SecuredAuthorizationManagerTests { - @Test - public void setAuthoritiesAuthorizationManagerWhenNullThenException() { - SecuredAuthorizationManager manager = new SecuredAuthorizationManager(); - assertThatIllegalArgumentException().isThrownBy(() -> manager.setAuthoritiesAuthorizationManager(null)) - .withMessage("authoritiesAuthorizationManager cannot be null"); - } - - @Test - public void setAuthoritiesAuthorizationManagerWhenNotNullThenVerifyUsage() throws Exception { - AuthorizationManager> authoritiesAuthorizationManager = mock(AuthorizationManager.class); - SecuredAuthorizationManager manager = new SecuredAuthorizationManager(); - manager.setAuthoritiesAuthorizationManager(authoritiesAuthorizationManager); - MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, - "securedUserOrAdmin"); - Supplier authentication = TestAuthentication::authenticatedUser; - AuthorizationDecision decision = manager.check(authentication, methodInvocation); - assertThat(decision).isNull(); - verify(authoritiesAuthorizationManager).check(authentication, Set.of("ROLE_USER", "ROLE_ADMIN")); - } - @Test public void checkDoSomethingWhenNoSecuredAnnotationThenNullDecision() throws Exception { MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, diff --git a/core/src/test/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutorIntegrationTests.java b/core/src/test/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutorIntegrationTests.java deleted file mode 100644 index 92098ed79b..0000000000 --- a/core/src/test/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutorIntegrationTests.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2020-2023 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.concurrent; - -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.JRE; - -import org.springframework.core.task.VirtualThreadTaskExecutor; -import org.springframework.security.DelegatingSecurityContextTestUtils; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Steve Riesenberg - */ -public class DelegatingSecurityContextExecutorIntegrationTests { - - @Test - public void executeWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void executeWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeAndReturn(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext executeAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.runAndReturn( - threadFactory, - this::createExecutor, - Executor::execute - ); - // @formatter:on - } - - private DelegatingSecurityContextExecutor createExecutor(ScheduledExecutorService delegate) { - return new DelegatingSecurityContextExecutor(delegate, securityContext()); - } - - private static SecurityContext securityContext() { - SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(new TestingAuthenticationToken("user", null)); - - return securityContext; - } - -} diff --git a/core/src/test/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutorServiceIntegrationTests.java b/core/src/test/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutorServiceIntegrationTests.java deleted file mode 100644 index 55df678328..0000000000 --- a/core/src/test/java/org/springframework/security/concurrent/DelegatingSecurityContextExecutorServiceIntegrationTests.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2020-2023 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.concurrent; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.JRE; - -import org.springframework.core.task.VirtualThreadTaskExecutor; -import org.springframework.security.DelegatingSecurityContextTestUtils; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Steve Riesenberg - */ -public class DelegatingSecurityContextExecutorServiceIntegrationTests { - - @Test - public void executeWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void executeWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeAndReturn(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext executeAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.runAndReturn( - threadFactory, - this::createExecutor, - ExecutorService::execute - ); - // @formatter:on - } - - @Test - public void submitWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = submitAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void submitWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = submitAndReturn(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext submitAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.callAndReturn( - threadFactory, - this::createExecutor, - ExecutorService::submit - ); - // @formatter:on - } - - private DelegatingSecurityContextExecutorService createExecutor(ScheduledExecutorService delegate) { - return new DelegatingSecurityContextExecutorService(delegate, securityContext()); - } - - private static SecurityContext securityContext() { - SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(new TestingAuthenticationToken("user", null)); - - return securityContext; - } - -} diff --git a/core/src/test/java/org/springframework/security/concurrent/DelegatingSecurityContextScheduledExecutorServiceIntegrationTests.java b/core/src/test/java/org/springframework/security/concurrent/DelegatingSecurityContextScheduledExecutorServiceIntegrationTests.java deleted file mode 100644 index 0a6ddc2dd5..0000000000 --- a/core/src/test/java/org/springframework/security/concurrent/DelegatingSecurityContextScheduledExecutorServiceIntegrationTests.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2020-2023 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.concurrent; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.JRE; - -import org.springframework.core.task.VirtualThreadTaskExecutor; -import org.springframework.security.DelegatingSecurityContextTestUtils; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Steve Riesenberg - */ -public class DelegatingSecurityContextScheduledExecutorServiceIntegrationTests { - - @Test - public void executeWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void executeWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeAndReturn(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext executeAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.runAndReturn( - threadFactory, - this::createExecutor, - ScheduledExecutorService::execute - ); - // @formatter:on - } - - @Test - public void submitWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = submitAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void submitWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = submitAndReturn(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext submitAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.callAndReturn( - threadFactory, - this::createExecutor, - ScheduledExecutorService::submit - ); - // @formatter:on - } - - @Test - public void scheduleWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = scheduleAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void scheduleWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = scheduleAndReturn(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext scheduleAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.callAndReturn( - threadFactory, - this::createExecutor, - (executor, task) -> executor.schedule(task, 50, TimeUnit.MILLISECONDS) - ); - // @formatter:on - } - - private DelegatingSecurityContextScheduledExecutorService createExecutor(ScheduledExecutorService delegate) { - return new DelegatingSecurityContextScheduledExecutorService(delegate, securityContext()); - } - - private static SecurityContext securityContext() { - SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(new TestingAuthenticationToken("user", null)); - - return securityContext; - } - -} diff --git a/core/src/test/java/org/springframework/security/core/JavaVersionTests.java b/core/src/test/java/org/springframework/security/core/JavaVersionTests.java index 3aab2520a6..de0621cf12 100644 --- a/core/src/test/java/org/springframework/security/core/JavaVersionTests.java +++ b/core/src/test/java/org/springframework/security/core/JavaVersionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,11 +32,11 @@ public class JavaVersionTests { private static final int JDK17_CLASS_VERSION = 61; @Test - public void authenticationWhenJdk17ThenCorrectJdkCompatibility() throws Exception { - assertClassVersion(Authentication.class, JDK17_CLASS_VERSION); + public void authenticationCorrectJdkCompatibility() throws Exception { + assertClassVersion(Authentication.class); } - private void assertClassVersion(Class clazz, int classVersion) throws Exception { + private void assertClassVersion(Class clazz) throws Exception { String classResourceName = clazz.getName().replaceAll("\\.", "/") + ".class"; try (InputStream input = Thread.currentThread() .getContextClassLoader() @@ -45,7 +45,7 @@ public class JavaVersionTests { data.readInt(); data.readShort(); // minor int major = data.readShort(); - assertThat(major).isEqualTo(classVersion); + assertThat(major).isEqualTo(JDK17_CLASS_VERSION); } } diff --git a/core/src/test/java/org/springframework/security/core/StaticFinalReflectionUtils.java b/core/src/test/java/org/springframework/security/core/StaticFinalReflectionUtils.java index 76e8b7eed4..1cff222680 100644 --- a/core/src/test/java/org/springframework/security/core/StaticFinalReflectionUtils.java +++ b/core/src/test/java/org/springframework/security/core/StaticFinalReflectionUtils.java @@ -68,7 +68,13 @@ final class StaticFinalReflectionUtils { field.set(null, newValue); } } - catch (SecurityException | IllegalAccessException | IllegalArgumentException ex) { + catch (SecurityException ex) { + throw new RuntimeException(ex); + } + catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + catch (IllegalArgumentException ex) { throw new RuntimeException(ex); } } diff --git a/core/src/test/java/org/springframework/security/core/authority/AuthorityUtilsTests.java b/core/src/test/java/org/springframework/security/core/authority/AuthorityUtilsTests.java index 46afc94346..a1813cfcd6 100644 --- a/core/src/test/java/org/springframework/security/core/authority/AuthorityUtilsTests.java +++ b/core/src/test/java/org/springframework/security/core/authority/AuthorityUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.security.core.authority; -import java.util.Arrays; import java.util.List; import java.util.Set; @@ -28,7 +27,6 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Luke Taylor - * @author Evgeniy Cheban */ public class AuthorityUtilsTests { @@ -37,21 +35,11 @@ public class AuthorityUtilsTests { List authorityArray = AuthorityUtils .commaSeparatedStringToAuthorityList(" ROLE_A, B, C, ROLE_D\n,\n E "); Set authorities = AuthorityUtils.authorityListToSet(authorityArray); - assertThat(authorities).contains("B"); - assertThat(authorities).contains("C"); - assertThat(authorities).contains("E"); - assertThat(authorities).contains("ROLE_A"); - assertThat(authorities).contains("ROLE_D"); - } - - @Test - public void createAuthorityList() { - List authorities = AuthorityUtils - .createAuthorityList(Arrays.asList("ROLE_A", "ROLE_B", "ROLE_C")); - assertThat(authorities).hasSize(3); - assertThat(authorities).element(0).extracting(GrantedAuthority::getAuthority).isEqualTo("ROLE_A"); - assertThat(authorities).element(1).extracting(GrantedAuthority::getAuthority).isEqualTo("ROLE_B"); - assertThat(authorities).element(2).extracting(GrantedAuthority::getAuthority).isEqualTo("ROLE_C"); + assertThat(authorities.contains("B")).isTrue(); + assertThat(authorities.contains("C")).isTrue(); + assertThat(authorities.contains("E")).isTrue(); + assertThat(authorities.contains("ROLE_A")).isTrue(); + assertThat(authorities.contains("ROLE_D")).isTrue(); } } diff --git a/core/src/test/java/org/springframework/security/core/authority/mapping/SimpleAuthoritiesMapperTests.java b/core/src/test/java/org/springframework/security/core/authority/mapping/SimpleAuthoritiesMapperTests.java index 4634c23af2..20dd15bd10 100644 --- a/core/src/test/java/org/springframework/security/core/authority/mapping/SimpleAuthoritiesMapperTests.java +++ b/core/src/test/java/org/springframework/security/core/authority/mapping/SimpleAuthoritiesMapperTests.java @@ -45,8 +45,8 @@ public class SimpleAuthoritiesMapperTests { SimpleAuthorityMapper mapper = new SimpleAuthorityMapper(); Set mapped = AuthorityUtils .authorityListToSet(mapper.mapAuthorities(AuthorityUtils.createAuthorityList("AaA", "ROLE_bbb"))); - assertThat(mapped).contains("ROLE_AaA"); - assertThat(mapped).contains("ROLE_bbb"); + assertThat(mapped.contains("ROLE_AaA")).isTrue(); + assertThat(mapped.contains("ROLE_bbb")).isTrue(); } @Test @@ -56,19 +56,19 @@ public class SimpleAuthoritiesMapperTests { List toMap = AuthorityUtils.createAuthorityList("AaA", "Bbb"); Set mapped = AuthorityUtils.authorityListToSet(mapper.mapAuthorities(toMap)); assertThat(mapped).hasSize(2); - assertThat(mapped).contains("AaA"); - assertThat(mapped).contains("Bbb"); + assertThat(mapped.contains("AaA")).isTrue(); + assertThat(mapped.contains("Bbb")).isTrue(); mapper.setConvertToLowerCase(true); mapped = AuthorityUtils.authorityListToSet(mapper.mapAuthorities(toMap)); assertThat(mapped).hasSize(2); - assertThat(mapped).contains("aaa"); - assertThat(mapped).contains("bbb"); + assertThat(mapped.contains("aaa")).isTrue(); + assertThat(mapped.contains("bbb")).isTrue(); mapper.setConvertToLowerCase(false); mapper.setConvertToUpperCase(true); mapped = AuthorityUtils.authorityListToSet(mapper.mapAuthorities(toMap)); assertThat(mapped).hasSize(2); - assertThat(mapped).contains("AAA"); - assertThat(mapped).contains("BBB"); + assertThat(mapped.contains("AAA")).isTrue(); + assertThat(mapped.contains("BBB")).isTrue(); } @Test @@ -86,7 +86,7 @@ public class SimpleAuthoritiesMapperTests { mapper.setDefaultAuthority("ROLE_USER"); Set mapped = AuthorityUtils.authorityListToSet(mapper.mapAuthorities(AuthorityUtils.NO_AUTHORITIES)); assertThat(mapped).hasSize(1); - assertThat(mapped).contains("ROLE_USER"); + assertThat(mapped.contains("ROLE_USER")).isTrue(); } } diff --git a/core/src/test/java/org/springframework/security/core/context/ReactiveSecurityContextHolderTests.java b/core/src/test/java/org/springframework/security/core/context/ReactiveSecurityContextHolderTests.java index d5c7127dc1..af89ece4c9 100644 --- a/core/src/test/java/org/springframework/security/core/context/ReactiveSecurityContextHolderTests.java +++ b/core/src/test/java/org/springframework/security/core/context/ReactiveSecurityContextHolderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2017 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,17 +16,10 @@ package org.springframework.security.core.context; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; - import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.JRE; import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; -import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; @@ -106,53 +99,4 @@ public class ReactiveSecurityContextHolderTests { // @formatter:on } - @Test - public void getContextWhenThreadFactoryIsPlatformThenPropagated() { - verifySecurityContextIsPropagated(Executors.defaultThreadFactory()); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void getContextWhenThreadFactoryIsVirtualThenPropagated() { - verifySecurityContextIsPropagated(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - } - - private static void verifySecurityContextIsPropagated(ThreadFactory threadFactory) { - Authentication authentication = new TestingAuthenticationToken("user", null); - - // @formatter:off - Mono publisher = ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) - .contextWrite((context) -> ReactiveSecurityContextHolder.withAuthentication(authentication)) - .subscribeOn(Schedulers.newSingle(threadFactory)); - // @formatter:on - - StepVerifier.create(publisher).expectNext(authentication).verifyComplete(); - } - - @Test - public void clearContextWhenThreadFactoryIsPlatformThenCleared() { - verifySecurityContextIsCleared(Executors.defaultThreadFactory()); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void clearContextWhenThreadFactoryIsVirtualThenCleared() { - verifySecurityContextIsCleared(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - } - - private static void verifySecurityContextIsCleared(ThreadFactory threadFactory) { - Authentication authentication = new TestingAuthenticationToken("user", null); - - // @formatter:off - Mono publisher = ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) - .contextWrite(ReactiveSecurityContextHolder.clearContext()) - .contextWrite((context) -> ReactiveSecurityContextHolder.withAuthentication(authentication)) - .subscribeOn(Schedulers.newSingle(threadFactory)); - // @formatter:on - - StepVerifier.create(publisher).verifyComplete(); - } - } diff --git a/core/src/test/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategyTests.java b/core/src/test/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategyTests.java index 0b3fbc35cb..9fc4b66401 100644 --- a/core/src/test/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategyTests.java +++ b/core/src/test/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategyTests.java @@ -57,7 +57,7 @@ class ThreadLocalSecurityContextHolderStrategyTests { void deferredContextValidates() { this.strategy.setDeferredContext(() -> null); Supplier deferredContext = this.strategy.getDeferredContext(); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(deferredContext::get); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> deferredContext.get()); } @Test diff --git a/core/src/test/java/org/springframework/security/core/parameters/DefaultSecurityParameterNameDiscovererTests.java b/core/src/test/java/org/springframework/security/core/parameters/DefaultSecurityParameterNameDiscovererTests.java index ebf3094b47..10e28b45b9 100644 --- a/core/src/test/java/org/springframework/security/core/parameters/DefaultSecurityParameterNameDiscovererTests.java +++ b/core/src/test/java/org/springframework/security/core/parameters/DefaultSecurityParameterNameDiscovererTests.java @@ -24,8 +24,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.core.StandardReflectionParameterNameDiscoverer; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -60,11 +60,11 @@ public class DefaultSecurityParameterNameDiscovererTests { @Test public void constructorDiscoverers() { this.discoverer = new DefaultSecurityParameterNameDiscoverer( - Arrays.asList(new StandardReflectionParameterNameDiscoverer())); + Arrays.asList(new LocalVariableTableParameterNameDiscoverer())); List discoverers = (List) ReflectionTestUtils .getField(this.discoverer, "parameterNameDiscoverers"); assertThat(discoverers).hasSize(3); - assertThat(discoverers.get(0)).isInstanceOf(StandardReflectionParameterNameDiscoverer.class); + assertThat(discoverers.get(0)).isInstanceOf(LocalVariableTableParameterNameDiscoverer.class); ParameterNameDiscoverer annotationDisc = discoverers.get(1); assertThat(annotationDisc).isInstanceOf(AnnotationParameterNameDiscoverer.class); Set annotationsToUse = (Set) ReflectionTestUtils.getField(annotationDisc, diff --git a/core/src/test/java/org/springframework/security/core/session/SessionRegistryImplTests.java b/core/src/test/java/org/springframework/security/core/session/SessionRegistryImplTests.java index 9e5e2ecf47..ecd563b946 100644 --- a/core/src/test/java/org/springframework/security/core/session/SessionRegistryImplTests.java +++ b/core/src/test/java/org/springframework/security/core/session/SessionRegistryImplTests.java @@ -97,8 +97,8 @@ public class SessionRegistryImplTests { this.sessionRegistry.registerNewSession(sessionId2, principal1); this.sessionRegistry.registerNewSession(sessionId3, principal2); assertThat(this.sessionRegistry.getAllPrincipals()).hasSize(2); - assertThat(this.sessionRegistry.getAllPrincipals()).contains(principal1); - assertThat(this.sessionRegistry.getAllPrincipals()).contains(principal2); + assertThat(this.sessionRegistry.getAllPrincipals().contains(principal1)).isTrue(); + assertThat(this.sessionRegistry.getAllPrincipals().contains(principal2)).isTrue(); } @Test diff --git a/core/src/test/java/org/springframework/security/core/userdetails/UserTests.java b/core/src/test/java/org/springframework/security/core/userdetails/UserTests.java index aa2a2387f6..f99de278d1 100644 --- a/core/src/test/java/org/springframework/security/core/userdetails/UserTests.java +++ b/core/src/test/java/org/springframework/security/core/userdetails/UserTests.java @@ -18,17 +18,12 @@ package org.springframework.security.core.userdetails; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; -import java.util.ArrayList; -import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Function; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullSource; -import org.junit.jupiter.params.provider.ValueSource; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; @@ -42,7 +37,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException * Tests {@link User}. * * @author Ben Alex - * @author Ilya Starchenko */ public class UserTests { @@ -74,70 +68,6 @@ public class UserTests { .isThrownBy(() -> User.class.getDeclaredConstructor((Class[]) null)); } - @Test - public void testBuildUserWithNoAuthorities() { - UserDetails user = User.builder().username("user").password("password").build(); - assertThat(user.getAuthorities()).isEmpty(); - } - - @Test - public void testNullWithinUserAuthoritiesIsRejected() { - assertThatIllegalArgumentException().isThrownBy(() -> User.builder() - .username("user") - .password("password") - .authorities((Collection) null) - .build()); - List authorities = new ArrayList<>(); - authorities.add(null); - authorities.add(null); - assertThatIllegalArgumentException() - .isThrownBy(() -> User.builder().username("user").password("password").authorities(authorities).build()); - - assertThatIllegalArgumentException().isThrownBy(() -> User.builder() - .username("user") - .password("password") - .authorities((GrantedAuthority[]) null) - .build()); - assertThatIllegalArgumentException().isThrownBy(() -> User.builder() - .username("user") - .password("password") - .authorities(new GrantedAuthority[] { null, null }) - .build()); - - assertThatIllegalArgumentException().isThrownBy( - () -> User.builder().username("user").password("password").authorities((String[]) null).build()); - assertThatIllegalArgumentException().isThrownBy(() -> User.builder() - .username("user") - .password("password") - .authorities(new String[] { null, null }) - .build()); - } - - // gh-12533 - @ParameterizedTest - @NullSource - @ValueSource(strings = { "ROLE_USER,ROLE_ADMIN,read", "read" }) - public void withUserDetailsWhenAuthoritiesThenOverridesPreviousAuthorities(String arg) { - // @formatter:off - UserDetails parent = User.builder() - .username("user") - .password("password") - .authorities("one", "two", "three") - .build(); - // @formatter:on - String[] authorities = (arg != null) ? arg.split(",") : new String[0]; - User.UserBuilder builder = User.withUserDetails(parent); - UserDetails user = builder.build(); - assertThat(AuthorityUtils.authorityListToSet(user.getAuthorities())).containsOnly("one", "two", "three"); - user = builder.authorities(authorities).build(); - assertThat(AuthorityUtils.authorityListToSet(user.getAuthorities())).containsOnly(authorities); - user = builder.authorities(AuthorityUtils.createAuthorityList(authorities)).build(); - assertThat(AuthorityUtils.authorityListToSet(user.getAuthorities())).containsOnly(authorities); - user = builder.authorities(AuthorityUtils.createAuthorityList(authorities).toArray(GrantedAuthority[]::new)) - .build(); - assertThat(AuthorityUtils.authorityListToSet(user.getAuthorities())).containsOnly(authorities); - } - @Test public void testNullValuesRejected() { assertThatIllegalArgumentException().isThrownBy(() -> new User(null, "koala", true, true, true, true, ROLE_12)); diff --git a/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java b/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java index 67d0c83fa5..38d835dcdf 100644 --- a/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java +++ b/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java @@ -145,7 +145,7 @@ public class JdbcUserDetailsManagerTests { AuthorityUtils.createAuthorityList("A", "B")); this.manager.createUser(user); UserDetails user2 = this.manager.loadUserByUsername(user.getUsername()); - assertThat(user2).usingRecursiveComparison().isEqualTo(user); + assertThat(user2).isEqualToComparingFieldByField(user); } @Test @@ -176,7 +176,7 @@ public class JdbcUserDetailsManagerTests { AuthorityUtils.createAuthorityList("D", "F", "E")); this.manager.updateUser(newJoe); UserDetails joe = this.manager.loadUserByUsername(newJoe.getUsername()); - assertThat(joe).usingRecursiveComparison().isEqualTo(newJoe); + assertThat(joe).isEqualToComparingFieldByField(newJoe); assertThat(this.cache.getUserMap().containsKey(newJoe.getUsername())).isFalse(); } @@ -189,7 +189,7 @@ public class JdbcUserDetailsManagerTests { public void userExistsReturnsTrueForExistingUsername() { insertJoe(); assertThat(this.manager.userExists("joe")).isTrue(); - assertThat(this.cache.getUserMap()).containsKey("joe"); + assertThat(this.cache.getUserMap().containsKey("joe")).isTrue(); } @Test @@ -251,7 +251,7 @@ public class JdbcUserDetailsManagerTests { UserDetails newJoe = this.manager.loadUserByUsername("joe"); assertThat(newJoe.getPassword()).isEqualTo("password"); assertThat(SecurityContextHolder.getContext().getAuthentication().getCredentials()).isEqualTo("password"); - assertThat(this.cache.getUserMap()).containsKey("joe"); + assertThat(this.cache.getUserMap().containsKey("joe")).isTrue(); } @Test diff --git a/core/src/test/java/org/springframework/security/scheduling/DelegatingSecurityContextSchedulingTaskExecutorIntegrationTests.java b/core/src/test/java/org/springframework/security/scheduling/DelegatingSecurityContextSchedulingTaskExecutorIntegrationTests.java deleted file mode 100644 index e77a7cffa5..0000000000 --- a/core/src/test/java/org/springframework/security/scheduling/DelegatingSecurityContextSchedulingTaskExecutorIntegrationTests.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2020-2023 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.scheduling; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.JRE; - -import org.springframework.core.task.VirtualThreadTaskExecutor; -import org.springframework.scheduling.SchedulingTaskExecutor; -import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; -import org.springframework.security.DelegatingSecurityContextTestUtils; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Steve Riesenberg - */ -public class DelegatingSecurityContextSchedulingTaskExecutorIntegrationTests { - - @Test - public void executeWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void executeWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeAndReturn(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext executeAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.runAndReturn( - threadFactory, - this::createExecutor, - SchedulingTaskExecutor::execute - ); - // @formatter:on - } - - @Test - public void executeCompletableWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeCompletableAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void executeCompletableWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeCompletableAndReturn( - new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext executeCompletableAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.runAndReturn( - threadFactory, - this::createExecutor, - SchedulingTaskExecutor::submitCompletable - ); - // @formatter:on - } - - @Test - public void submitWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = submitAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void submitWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = submitAndReturn(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext submitAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.callAndReturn( - threadFactory, - this::createExecutor, - SchedulingTaskExecutor::submit - ); - // @formatter:on - } - - @Test - public void submitCompletableWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = submitCompletableAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void submitCompletableWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = submitCompletableAndReturn( - new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext submitCompletableAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.callAndReturn( - threadFactory, - this::createExecutor, - SchedulingTaskExecutor::submitCompletable - ); - // @formatter:on - } - - private DelegatingSecurityContextSchedulingTaskExecutor createExecutor(ScheduledExecutorService delegate) { - return new DelegatingSecurityContextSchedulingTaskExecutor(new ConcurrentTaskExecutor(delegate), - securityContext()); - } - - private static SecurityContext securityContext() { - SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(new TestingAuthenticationToken("user", null)); - - return securityContext; - } - -} diff --git a/core/src/test/java/org/springframework/security/scheduling/DelegatingSecurityContextTaskSchedulerIntegrationTests.java b/core/src/test/java/org/springframework/security/scheduling/DelegatingSecurityContextTaskSchedulerIntegrationTests.java deleted file mode 100644 index aff48a6d8a..0000000000 --- a/core/src/test/java/org/springframework/security/scheduling/DelegatingSecurityContextTaskSchedulerIntegrationTests.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2020-2023 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.scheduling; - -import java.time.Duration; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.JRE; - -import org.springframework.core.task.VirtualThreadTaskExecutor; -import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; -import org.springframework.scheduling.support.PeriodicTrigger; -import org.springframework.security.DelegatingSecurityContextTestUtils; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Steve Riesenberg - */ -public class DelegatingSecurityContextTaskSchedulerIntegrationTests { - - @Test - public void scheduleWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = scheduleAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void scheduleWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = scheduleAndReturn(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext scheduleAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.runAndReturn( - threadFactory, - this::createTaskScheduler, - (taskScheduler, task) -> taskScheduler.schedule(task, new PeriodicTrigger(Duration.ofMillis(50))) - ); - // @formatter:on - } - - @Test - public void scheduleAtFixedRateWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = scheduleAtFixedRateAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void scheduleAtFixedRateWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = scheduleAtFixedRateAndReturn( - new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext scheduleAtFixedRateAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.runAndReturn( - threadFactory, - this::createTaskScheduler, - (taskScheduler, task) -> taskScheduler.scheduleAtFixedRate(task, Duration.ofMillis(50)) - ); - // @formatter:on - } - - @Test - public void scheduleWithFixedDelayWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = scheduleWithFixedDelayAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void scheduleWithFixedDelayWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = scheduleWithFixedDelayAndReturn( - new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext scheduleWithFixedDelayAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.runAndReturn( - threadFactory, - this::createTaskScheduler, - (taskScheduler, task) -> taskScheduler.scheduleWithFixedDelay(task, Duration.ofMillis(50)) - ); - // @formatter:on - } - - private DelegatingSecurityContextTaskScheduler createTaskScheduler(ScheduledExecutorService delegate) { - return new DelegatingSecurityContextTaskScheduler(new ConcurrentTaskScheduler(delegate), securityContext()); - } - - private static SecurityContext securityContext() { - SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(new TestingAuthenticationToken("user", null)); - - return securityContext; - } - -} diff --git a/core/src/test/java/org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutorIntegrationTests.java b/core/src/test/java/org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutorIntegrationTests.java deleted file mode 100644 index 1fd44da083..0000000000 --- a/core/src/test/java/org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutorIntegrationTests.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2020-2023 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.task; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.JRE; - -import org.springframework.core.task.AsyncTaskExecutor; -import org.springframework.core.task.VirtualThreadTaskExecutor; -import org.springframework.core.task.support.TaskExecutorAdapter; -import org.springframework.security.DelegatingSecurityContextTestUtils; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Steve Riesenberg - */ -public class DelegatingSecurityContextAsyncTaskExecutorIntegrationTests { - - @Test - public void executeWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void executeWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeAndReturn(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext executeAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.runAndReturn(threadFactory, - this::createExecutor, - AsyncTaskExecutor::execute - ); - // @formatter:on - } - - @Test - public void executeCompletableWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeCompletableAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void executeCompletableWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeCompletableAndReturn( - new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext executeCompletableAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.runAndReturn(threadFactory, - this::createExecutor, - AsyncTaskExecutor::submitCompletable - ); - // @formatter:on - } - - @Test - public void submitWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = submitAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void submitWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = submitAndReturn(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext submitAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.callAndReturn(threadFactory, - this::createExecutor, - AsyncTaskExecutor::submit - ); - // @formatter:on - } - - @Test - public void submitCompletableWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = submitCompletableAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void submitCompletableWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = submitCompletableAndReturn( - new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext submitCompletableAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.callAndReturn(threadFactory, - this::createExecutor, - AsyncTaskExecutor::submitCompletable - ); - // @formatter:on - } - - private DelegatingSecurityContextAsyncTaskExecutor createExecutor(ScheduledExecutorService delegate) { - return new DelegatingSecurityContextAsyncTaskExecutor(new TaskExecutorAdapter(delegate), securityContext()); - } - - private static SecurityContext securityContext() { - SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(new TestingAuthenticationToken("user", null)); - - return securityContext; - } - -} diff --git a/core/src/test/java/org/springframework/security/task/DelegatingSecurityContextTaskExecutorIntegrationTests.java b/core/src/test/java/org/springframework/security/task/DelegatingSecurityContextTaskExecutorIntegrationTests.java deleted file mode 100644 index 6d0f1a992c..0000000000 --- a/core/src/test/java/org/springframework/security/task/DelegatingSecurityContextTaskExecutorIntegrationTests.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2020-2023 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.task; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.JRE; - -import org.springframework.core.task.TaskExecutor; -import org.springframework.core.task.VirtualThreadTaskExecutor; -import org.springframework.core.task.support.TaskExecutorAdapter; -import org.springframework.security.DelegatingSecurityContextTestUtils; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Steve Riesenberg - */ -public class DelegatingSecurityContextTaskExecutorIntegrationTests { - - @Test - public void executeWhenThreadFactoryIsPlatformThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeAndReturn(Executors.defaultThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void executeWhenThreadFactoryIsVirtualThenSecurityContextPropagated() throws Exception { - SecurityContext securityContext = executeAndReturn(new VirtualThreadTaskExecutor().getVirtualThreadFactory()); - assertThat(securityContext.getAuthentication()).isNotNull(); - } - - private SecurityContext executeAndReturn(ThreadFactory threadFactory) throws Exception { - // @formatter:off - return DelegatingSecurityContextTestUtils.runAndReturn(threadFactory, - this::createExecutor, - TaskExecutor::execute - ); - // @formatter:on - } - - private DelegatingSecurityContextTaskExecutor createExecutor(ScheduledExecutorService delegate) { - return new DelegatingSecurityContextTaskExecutor(new TaskExecutorAdapter(delegate), securityContext()); - } - - private static SecurityContext securityContext() { - SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(new TestingAuthenticationToken("user", null)); - - return securityContext; - } - -} diff --git a/crypto/src/main/java/org/springframework/security/crypto/argon2/Argon2EncodingUtils.java b/crypto/src/main/java/org/springframework/security/crypto/argon2/Argon2EncodingUtils.java index 4b27d90318..883ab530f4 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/argon2/Argon2EncodingUtils.java +++ b/crypto/src/main/java/org/springframework/security/crypto/argon2/Argon2EncodingUtils.java @@ -58,13 +58,19 @@ final class Argon2EncodingUtils { */ static String encode(byte[] hash, Argon2Parameters parameters) throws IllegalArgumentException { StringBuilder stringBuilder = new StringBuilder(); - String type = switch (parameters.getType()) { - case Argon2Parameters.ARGON2_d -> "$argon2d"; - case Argon2Parameters.ARGON2_i -> "$argon2i"; - case Argon2Parameters.ARGON2_id -> "$argon2id"; - default -> throw new IllegalArgumentException("Invalid algorithm type: " + parameters.getType()); - }; - stringBuilder.append(type); + switch (parameters.getType()) { + case Argon2Parameters.ARGON2_d: + stringBuilder.append("$argon2d"); + break; + case Argon2Parameters.ARGON2_i: + stringBuilder.append("$argon2i"); + break; + case Argon2Parameters.ARGON2_id: + stringBuilder.append("$argon2id"); + break; + default: + throw new IllegalArgumentException("Invalid algorithm type: " + parameters.getType()); + } stringBuilder.append("$v=") .append(parameters.getVersion()) .append("$m=") @@ -107,12 +113,19 @@ final class Argon2EncodingUtils { throw new IllegalArgumentException("Invalid encoded Argon2-hash"); } int currentPart = 1; - paramsBuilder = switch (parts[currentPart++]) { - case "argon2d" -> new Argon2Parameters.Builder(Argon2Parameters.ARGON2_d); - case "argon2i" -> new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i); - case "argon2id" -> new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id); - default -> throw new IllegalArgumentException("Invalid algorithm type: " + parts[0]); - }; + switch (parts[currentPart++]) { + case "argon2d": + paramsBuilder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_d); + break; + case "argon2i": + paramsBuilder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i); + break; + case "argon2id": + paramsBuilder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id); + break; + default: + throw new IllegalArgumentException("Invalid algorithm type: " + parts[0]); + } if (parts[currentPart].startsWith("v=")) { paramsBuilder.withVersion(Integer.parseInt(parts[currentPart].substring(2))); currentPart++; diff --git a/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTests.java b/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTests.java index 2774cb006d..f0d60b4cba 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptTests.java @@ -390,7 +390,7 @@ public class BCryptTests { Arrays.fill(ba, (byte) 0); ba[i] = (byte) b; String s = encode_base64(ba, 3); - assertThat(s).hasSize(4); + assertThat(s.length()).isEqualTo(4); byte[] decoded = BCrypt.decode_base64(s, 3); assertThat(decoded).isEqualTo(ba); } diff --git a/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTests.java b/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTests.java index a37c83092c..66cc64b30e 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorEquivalencyTests.java @@ -20,6 +20,7 @@ import java.security.SecureRandom; import java.util.Random; import java.util.UUID; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,8 +29,6 @@ import org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgor import org.springframework.security.crypto.keygen.BytesKeyGenerator; import org.springframework.security.crypto.keygen.KeyGenerators; -import static org.assertj.core.api.Assertions.assertThat; - public class BouncyCastleAesBytesEncryptorEquivalencyTests { private byte[] testData; @@ -97,11 +96,11 @@ public class BouncyCastleAesBytesEncryptorEquivalencyTests { // and can decrypt back to the original input byte[] leftEncrypted = left.encrypt(this.testData); byte[] rightEncrypted = right.encrypt(this.testData); - assertThat(rightEncrypted).containsExactly(leftEncrypted); + Assertions.assertArrayEquals(leftEncrypted, rightEncrypted); byte[] leftDecrypted = left.decrypt(leftEncrypted); byte[] rightDecrypted = right.decrypt(rightEncrypted); - assertThat(leftDecrypted).containsExactly(this.testData); - assertThat(rightDecrypted).containsExactly(this.testData); + Assertions.assertArrayEquals(this.testData, leftDecrypted); + Assertions.assertArrayEquals(this.testData, rightDecrypted); } } @@ -115,8 +114,8 @@ public class BouncyCastleAesBytesEncryptorEquivalencyTests { byte[] rightEncrypted = right.encrypt(this.testData); byte[] leftDecrypted = left.decrypt(rightEncrypted); byte[] rightDecrypted = right.decrypt(leftEncrypted); - assertThat(leftDecrypted).containsExactly(this.testData); - assertThat(rightDecrypted).containsExactly(this.testData); + Assertions.assertArrayEquals(this.testData, leftDecrypted); + Assertions.assertArrayEquals(this.testData, rightDecrypted); } } diff --git a/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorTests.java b/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorTests.java index 792c7c89df..2b5ec62348 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/encrypt/BouncyCastleAesBytesEncryptorTests.java @@ -20,13 +20,13 @@ import java.security.SecureRandom; import java.util.UUID; import org.bouncycastle.util.Arrays; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.security.crypto.codec.Hex; import org.springframework.security.crypto.keygen.KeyGenerators; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; public class BouncyCastleAesBytesEncryptorTests { @@ -64,11 +64,11 @@ public class BouncyCastleAesBytesEncryptorTests { private void generatesDifferentCipherTexts(BytesEncryptor bcEncryptor) { byte[] encrypted1 = bcEncryptor.encrypt(this.testData); byte[] encrypted2 = bcEncryptor.encrypt(this.testData); - assertThat(Arrays.areEqual(encrypted1, encrypted2)).isFalse(); + Assertions.assertFalse(Arrays.areEqual(encrypted1, encrypted2)); byte[] decrypted1 = bcEncryptor.decrypt(encrypted1); byte[] decrypted2 = bcEncryptor.decrypt(encrypted2); - assertThat(decrypted1).containsExactly(this.testData); - assertThat(decrypted2).containsExactly(this.testData); + Assertions.assertArrayEquals(this.testData, decrypted1); + Assertions.assertArrayEquals(this.testData, decrypted2); } @Test diff --git a/crypto/src/test/java/org/springframework/security/crypto/keygen/KeyGeneratorsTests.java b/crypto/src/test/java/org/springframework/security/crypto/keygen/KeyGeneratorsTests.java index dcf44e34ca..517a01fb04 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/keygen/KeyGeneratorsTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/keygen/KeyGeneratorsTests.java @@ -60,7 +60,7 @@ public class KeyGeneratorsTests { public void string() { StringKeyGenerator keyGenerator = KeyGenerators.string(); String hexStringKey = keyGenerator.generateKey(); - assertThat(hexStringKey).hasSize(16); + assertThat(hexStringKey.length()).isEqualTo(16); assertThat(Hex.decode(hexStringKey)).hasSize(8); String hexStringKey2 = keyGenerator.generateKey(); assertThat(hexStringKey.equals(hexStringKey2)).isFalse(); diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java index 5c62fff4cf..487d312e85 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/password/Pbkdf2PasswordEncoderTests.java @@ -39,8 +39,8 @@ public class Pbkdf2PasswordEncoderTests { // encode output is an hex coded String so with 2 chars per encoding result byte // (ie. 1 char for 4 bits). // The encoding result size is : (saltLength * 8) bits + hashWith bits. - assertThat(this.encoder.encode("password")).hasSize((8 * 8 + 256) / 4); - assertThat(this.encoderSalt16.encode("password")).hasSize((16 * 8 + 256) / 4); + assertThat(this.encoder.encode("password").length()).isEqualTo((8 * 8 + 256) / 4); + assertThat(this.encoderSalt16.encode("password").length()).isEqualTo((16 * 8 + 256) / 4); } @Test diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 434d805583..82b6f11d0d 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -64,7 +64,6 @@ dependencies { api libs.org.hamcrest api libs.org.hibernate.orm.hibernate.core api libs.org.hsqldb - api libs.org.apereo.cas.client.cas.client.core api libs.org.opensaml.opensaml.core api libs.org.opensaml.opensaml.saml.api api libs.org.opensaml.opensaml.saml.impl @@ -83,4 +82,3 @@ dependencies { api libs.org.apache.maven.maven.resolver.provider } } - diff --git a/docs/modules/ROOT/assets/images/servlet/architecture/multi-securityfilterchain.odg b/docs/modules/ROOT/assets/images/servlet/architecture/multi-securityfilterchain.odg index 97edccb82db835e6782fd3f0db5dd6e70a7c59c3..c0dc6dc40c4647ae3ab4cc1cd990fe57653d517f 100644 GIT binary patch literal 14601 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 diff --git a/docs/modules/ROOT/assets/images/servlet/architecture/multi-securityfilterchain.png b/docs/modules/ROOT/assets/images/servlet/architecture/multi-securityfilterchain.png index 1481a120eb43ce2aa583676822884e5cb150b923..aa5195f1bbe61b1e3ab679b4930888ca7c3cdb91 100644 GIT binary patch literal 57970 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�puFCCD?+)=^1T~S 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∨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ŌX@Zp^{>fekEJ!e((Pv-yxg3w*-O4T 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 TU{=@(;LkNBEyX-}^Wgsn8@m~M diff --git a/docs/modules/ROOT/assets/images/servlet/authorization/methodsecurity.odg b/docs/modules/ROOT/assets/images/servlet/authorization/methodsecurity.odg deleted file mode 100644 index 8b5ffa68013dfd3af1d9bd7baef81fed2d713a1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38248 zcmcG#bBt%fw=eo_+qP{?+nBbyr)}G|ZQHhO+qP|U-u&*(xyegT&ddGdRd&^0sbtk! zm8x9}_1W@LAfTuK03-l_o0b137leL@1^@v3r~c~#SesiLJGt8#>)Y8`nH%amncLbh zxY!uc+v+=-JJQ?Q8QU1y8ai7W+c?piI_TS(nHxIF|Gx(OH^%=dnExnYTN@K|Q)h?& zHIySWgOjbTm4Utk!~Y$ZiIuIslkxv8*FQC#|7Tg)|BZ%rwsy{T|6%Iwu+z6Ow)!9B z{dd#UH#9W1GXA%ow*R#pkdTo7YcKtq+5aif|0qW%cPnE@dN*sUV{Ki>O*UlTUb%h+ z3iee94N}qZ1O{0OUvlf)geH{M)%G4YLga)sdC<@zT5jC~{M*lM3QpRexI7X0Jn2cB zgDDPl7oe-3IyFEf+4EQDrnA-d9&N0+1E<|=mVa=fUywpW7w*@`q^2j=+s&|UPU0{z z^qr`9z%({Ra5jc3(+r&k=QsmLJfZ`q8k(pF6K(=3Mb}f5(PaK9M41&qmgfXOnXOUsH1{O=x??CnbL%@}w(4YBf?f^yh%9 z_+jO7lO_W2a4KP@xV5b{J2y-e{8`3(M!=Y_VceE$Yz`^kmy9%Ttk`gvD~z-e7iDc8 z*K`o45nE{tjpkwo=Uy6XvI>be1*Og^m!JPNgl4Z)o&8hxC!Wr>bL) z$h3%O;=`5e7GSQkBklN*SPmUQ{tAw#q+5awsrp6SCOn~kh;tWtoE2tAdtJFyQ?Dd+ zu+5>d z;KbSUn4KnUkVds zR48?~eY{soN%p>rhOfg|XCVVz+dTw@hkh~f*xA*0i7kbV288E_lO#G77YboCA&%N` zQ`Jftb4vMElxm&(O3K_b&A8%M5{IuC;EMDdXvNkNy91vK#D%3^SR^*T%{3MdQgYAWSw;C4JDhYg$V^8MIo46RWRTYXe%F((iEf9hBuNZ9fnXucpakVkOf@T zz;gM_|6{J*%}1N8cLi6KKZ6(P;6v_tkDFrA@wF^*VN zLqk*!*qI9>xfB1>big+xvbLF|P@j*5vtGCKLs=eWTg;<$`I5BYII;%;Cy`D>0q3~T zTBt(Kz93l*@9Bq1SAT&G=COIyjaBln;#p`i@2g2 z5^pXMtV<}nP;ik#D9ipTJZn$&aA;01Ier;SJ#x~#R@tg`}5=NxwEjX$C| z&5>-(SaZRr(gb<}-!AiBAoFupQE{8SW{EEIe>pCV_9O`S! zf>#G<`88wpxRZ`m@q?3tll`8lDZ?!=!8Eh)TMK()VK#OIxwYbd`UnaV!H2XjiLYj;74s(!5YL!@M?ZLV_6m$s7maCm zNR|;?V^TR6t4O$Z*Oyn#q#q_Hf{5#6FD}|qFGivf%w5rK z=hYd>9dBj5(k{?2 zws$ll+ymMX`i*rpgUacAj|Ro0@f~KPSq45F=S$o{o-SVN5Xm*OeqvN z(yD_cj1Q%)ByWaLLy`N#FcD+^AO_1RLa42|^*jqyLCY3)fHH`a3c8KeykOW(ff39F zO1(*o3YCwQ2DKq_mJ+ry4TPp?+#vpNpr^V}fnw(u@}zbL$ltQ*y2$Oc59iKdLX{TG z-90t6T`%F-nc(4Y`I!~Om-s}hwznSKuiM_gRMSf4BK7irO!3HEqE3<&W1fR*{ikna zJcu|IYR(`RqGz*)Ku}gyT@8NH=*$M$WVrYCwm~3TsMj642(eNKz1$(AJ%q=OU4Dj% zj$Xs&o+3rews3@Y(*KGQv!27{&trtoy8FI)<@n&=|?ULY|d+{%U~M7M>a zgp~d)exroqDk!*QLIQBEF$pVlzFelz?5-z-b80?zy*4OTho62(RC&qFOWHl4hGKJ| zS1H0)3f3Hx0IUZi%=@+!htgE4H11{OBERg19+M!9AUqh0J%3{86LL{fLOZ zIH&fMn|6lS26DguH>l6n4Cbd)Q6<k1wjU9J1@IWNsD>_V!~tG?-=9pvrDTgxqL z64?9}dI7NsyKyqOv!yBQ3e(3tS2pa1Jpb-(!7c)3@WF&`X}+GjB$r3F6R5BSo{%Je zvWxDtlf6c1Ez=4{T8=H$5}|6OzNVUDf#{Db4_Q>It03Ye1lmJ zB!B&2E>jdURMTaf&#t{~Q!8&GRyp-Y!&J?<$wiab^Bj#_xuaX&5GGuD>@FG{QF;12 zmKpug8KrA-z%FaBCFh$q8Y*x)Kd^Ii$1*u@4O8uNaiVfzKWW zTDb`lJf`0SmFA_sJ$nixrzE*yrED7SVS+H^&M7E^#9aJX%>4HpH-ns}-&8$a40a_o z^zr9A_L%uPod~H~VjlO%gE8;PU;Z?S`6;s|&&*2@Uan9h@jIX5tV54#&rOE);IN_hiVY!0ipUway($W)}hGupN<*>}g8?*5$ zZ(beem>#6W6rl6|(XA}P`!fl#eXd=UCfeJZH6!O+3+&TtsHv|4WlLfbpX?np8K3sg zIJ=0=!%WK9lVd|Y?^0YGOPwaIU7ltl_9)Ol%9-$`EEd|eujAY_F5MMT+tHmtH$H=oDFOG7vD3m5=P5$0VzNRz zbRChWD6Xg()=I-PA^1mb?+!{;d|8s`8zAQpN$3m=TwG#9yg!nERGLCnO?EuJeE65& zmUJ+k&#s0>A1nz7F6f|Wn-FdsWOk;6T=nj_%2SePf(O)=UmNf|k~0+})r0$iyZ*XG zBz!I}+eWq)G;`QqhDDqH@_TjJ6ARdV65`@r!Q1kH(TnntY{S;_MX+<{9eDl(YV+YI z(}K%sgJ}D$(e`%eIpS8|y2aBk6X+H@wc113e^;lRYGMiA@O(iPv&)kur%<+j6Psmv zefsIJ^y#Lus%l7Ds~L6lti+0Zk3KaM(X+(iT5>+g|E+QM+Jv4mQwUe1>g&-)?p+gX zh2OmY{1N0fJ+SVy@j0U$tIGZet)3IbOVLWf!$j38|C_TUL+#|f>fGCv+sTkrO%}da z&Le2hFtJC{s-+O!PUS@;sVumJy8qjE_`K z`QMSZo@@9BWcy!mWRdK&8J|0yt2iBtnuk}=rtAuSwcYd1Aqtd{zv$gtH`ryIxq6-) z)w~U%3RQHKfoofX8Ju9PB;x4k>Sc8ae0db09*$X)2EHj%}2JX1Ok>&dL|(LFzCod!KmQSGTPk(-gg)C3lhY z@E|d*HZFLBI5XJ8gtnf0K>Y{`u9{B4 ztu}FxZL95l?<+3_0XegHBIJ(%05m%T0RKDt^4|sSF#iH~Lt7iC|73XnMef(y=Lwr_ z%|E%N^5zNUw4xD!*Bo=#X}1w)M?<;x5!suqFZ+54@q;vC%!CUvhWK3fULFa69a5iY z5oLI_&7#VU4H67OPvhR+9^m8UL?319Mn3C}qbmnQJ$+XG%wQHlb@j2=$BRV$F;lho z9i1VyC$fg;%7&d%nhYb9>YbOQdhm#vW*AdXHGdYubBQ1%&Y|7 z`Ha}BE*c&F%6UmWSe>E6s|4)!+lcM7(_5WG3uuikE~rVlR2TjE!BHaF;h&9 z3*Mcfr}!7TM*l&ot1W+rZWgrg?nx8lQ%{+&6wI=Xzu25X3EV`=dBcef`G;W zI{Hnky2h}HaqdSC$110wo^ow(Us(@aKKW za*!fP)J<734cV5M|LSajp?RY6G`lt3hHdSpPpafq(Iq->6Z)+<(dYdQ9slN2a?Hh0 zLR&q-Gh2|lwZ2}{V3DzvT|C;XDW^|XW#No2nvcwiWdkJvZMZK99Ug`RzC+;Aa%(zQ zQmpB^spR?d7sONLW9#)*AWFh(+<8IS@2r^7%lB2;phVj?uJyoo>9d5DE3SJGAcUvMtXD`Q}#bw>m_M5 zu-f6zN*m@cN|=!t{tw2S*X>gDqB9YJ1v^e?s~I&06dIp4_Z$ue9h4>nMO8>W-)QaU z-MKaQY}y}!j0`$)t;xj9>V+K73e1H`ah&WwQ3l~tZmW;C2bd^wMGsmXue@9gGz}Lv z4~iSl&pbm!69jX~z(k={xaOxr&Iwt4y75r9hl$4~A=K;(-h&~b2$?lMJQOcnVwpuu zKT*&Vm4=kLOGXbj ziw5G3ko<9Pq0e|`B?X>y@8eYw+nhgda^lJr$%tKV|e)2K+zyG|rm;v-pL zTfE)15yAX1=*HZ=z@DhE=d##Xj!Cm|bta+aS{?7Nkj28_#E(9fpo%f`6@cIriww}r zDsj?YP)5%qTO={VPEKf(udg}-eZg)#AWa+0xIpW$g96W>{rHT^DywN!cB>`A9M<`KY4#+sg#fYs!@LCpk}7 zh)=kKF_=DNw6(n=Bg_IJo8uaG!RSk}866J|oeNcS`eO}nO_s&C!Y}GQ@?d+_A>0`W zRLBS*C+ZIj`ZbX&y?x0L#a%Cm^Li?o$sxZRkS)YLSAc5w2ml*L6~A&~%rNz$?)cy83rMMx3aSF6v87FoqT8D>MJ5&%U$NTaDRU4;%-@9vr!X7v@ z>H#5Hhx~Uya;9wa;>yXaG=cM2ug{4&r0@HQ5)VI?PO5mCSxiSFT9S%G&|Z!xf989h z)B*jG0x#_6(zAEnPxxqx>#0p&c#r{YpdD`&)3fK#HnKu(2ByTAH`)O)t4c2wWNl&S zj}_pgF)BoXm`dolairh<^N8FCJTBoyVZ~6Q{te_YM)NdUY2xYD1%I-L2xzM({oz+& zlGhVGC*r7UA%nKA4joW#BnypL-q)l!T~5@{JErV;)xU8+ZbM&oljP*PmA8x!(iaxp zCt|3kYZhy+ge&3~*3{HwB&c-sKE2-Pc%Vuyq^s=Jga36jGBXP&Z%IwNJ`jQ6 zXIiW4XyEp-;M7F0h}MqH+?{1&hH>eTrCi0gQWT-c)GNLCR8MZ>8GgNRVq(0*J_INZ zN?$NV4ZZ5~ZXr-{mt%T6AC&U5u-LFirO>#tC~dk{ zi45eqZ&?w(FLyr}EniMVNWwEi_##CRfeiG9Iq?bxZ08fks&3hU6tFZ_#qm*8Wt$Q- z5{KhI_vZXP-Th15zt6Q#Xkcx|Lu6+oA<+&;4r;b(N0)X-C_Bu!#jrWM7;qQ%dJ+u2|6Ol*IFh$ zoZqk$kEF529r&G-)cm9IbKfKy3e-2mT3nlcH?Vck{saXc5Ldq|nN{S(!Mt>nKxat0 z8I!&SHd(nF9srQFGV82O2T#u=c~pfZ8u+6=e$CWz*?JD?5tMag?x#gbcx%*D z!J_0ua#X5diY&F3ctP}r43c+Myoq_Od+{kJ3lQUzY*S_1 z-LL5Us6(f{;Q_(z751#&ppU8qah=iMPxsLTh*TckE?pM0De1^28w&q{0*{TR0&&^u1{B8hc{=wH5H&FW-1mzj zZL54?a_b}4!HvLHo8+yD%eR%N>Ul0e>w7FNk`#vP9DVbaXk}5cK*Ey!RX1xO809!o zw48KO>uD1&LxWiy4V%LuVRs%-N!Qg(K&qeA+MPa&+&!s97f-wPp=s=$^~p#zm~Cv{ zV}%!fAjf5|SLaq%=lm#-xNcHSl!1GgupEZD!7-o1|h(K=YHX%Nwr`}Q_PpB)QKtvdAl%i%2#PIH1sy)cHPube8rt(u`DRA z{_}u3$ZN$oe>fdGRL`j*|gojPb=V;$3%al|0Qd;>^tK*4+uAs>Ki0o+5B@^|CrPj6zn-sW)O z0IdA}`;qF)0#eRYZqSXusNV=6mP-Jjt+#b$W2gWiW4$}-RYU_;E}^I;IioZqA;l1! z-nBD>&bC$tw<_5a?1pZb$J;i`Gz-x!xcJ@(^X5oTkj`lvmtOL_&wcdiozXPr9RyY2 zM(zxZOb4O_oAB4q_N{`8KR%tceajx4)N|LNY*$0a3hCAqJ6GX=os32%)Z<`wy~x-YJ;a z4MbH^;}_cVv24S)hRQV?k^E~Zv)mtK@};|v?XaO8yribLsSM(WLIGkjD+B{S7Qu3L zRrX14*HE66+YiMPE`!R1VHoj(iJI0d_X}KXDC3o{lc^mrQ1{Hwyi#6g)V{tw34?S-_66^XLz^OIJbUp9me(( z@`qmgsh^%nXpqBtntYhhV0O0w2`vem!uZ7Yvk2U!ZsbpZ`uoZ*^)Px%;tNE3jNrSr z3svPJRF?g%02b202)Dept5(fcf^qzFZUSZcxoN)aLKWk|x>z~A7JR4ZDqOh1*y-CN zCe>&k1bX&8T~z|)($b(gc4Y-=(U3?q$WFR&4c6X2`IADN^|rp6kQmXa`S`GKl;fj| z{}6-|^VpDfhLV6^6ix7vN!LBrTfF5yHo<_X2EU*7?)hc9NDt~0*3jM;IQr6}G_ zTwRSdRle1nvaj3BqW7pCnL@ToXV%c&+LCA2xSk>F61+Z+i#0cc4=QV2Q2S@}&M5gQ zMACN$Im|~DQJn~u;}OP?MHOE&R_=%jcE2aM3MrhzOS!4=i2vd|NIMe)joMshbIvA3 z1QMK%gInsLVW{vNIa`hK37YAk&qpb;4-GbOUxF*)Q<@Fc>XGC zm|?1c-8!Ux31dCkMW%#IpP~3VRWwm9Ij9+fKq?@(%_7t!o##E3{Tx(0@ z#b_)w_5pahlMutE)Qg{`g!V%9#VgvCmx>VLa#gH;^GNo2d{CRefyj+Tdy0~ajea05 z@CaoA@8Y-a_M+N zwu%KcR|>)ux@q zBiSS-F(~N($mfW%5@U6t=0J&cUikyWThziRQB(MWnvTe+49Z%dn+9VkDrg$@dW!H z#E7wzlevxQ|H6!HYgjrSaG-zp^cF;BRA-b|vxXEc`d=^_qv-Hv;t zHMk$G-Zm4R!vZDL-!HA42m{o)PA@J#F3vV~eRMyc7Lj2jI6CQ)_-KOV(m1!sraI}O zrcZ`t_Fp%8d+yhxINB~8C?1$#7)arT&xrZ^?Z_)PC$kI3_eIZ5yFL%(`VS<;3!V%_saG+cjsd}ScFxZUqtG=Fq;NPnqTC{qs~T{x~T z&h%_JzPzl8!EIK%7_8lVRwB03F6A8VbHZBex;U=;XvHCZ+DCb9{2DcC1!HXQAa#32 z<_#7;{NXYYGV0Q+=o>o{+kU?-Rsxw^J}I#4+nnH=Tchm;ws7Nv^kIxCF@ z9Tp@+7Za(-CD#OM&+Tv;%j=fu7=c8yB!sk2dnePs-V>?YXBsw0fK zJ0ChW$VjdB=ki?xoR9)%5z^<;+m$PVF{C7meR2!|=vpnL&sqYI-u zeGJ)l8|o-}PquJ0mBJg8mGT}+md-5Q%82l4WX}}rW{ecoV{5t}4bOd)#Lgib*;N0) zdChKoUWNrmMo2!F2%OVJBq+rgkF096W-&QN%#cBtybyxs_BRM?{?;$JY?V{~ z(ZHSee>l_(m&um3G#egwDMD(%Wc2hSPdVlNf*>ZzK zFbB`Q2J z9BId8h-YiB3%@jYao9CwNORfuLc!wz+}IFLEs#f3p6~~7;TNNBi1*>uF}%2x5m3^b zt&z~u6O{ZmV|+b&tHL?+{G#*M0@iCk=405NXi_*kN{u@;|8+>9R!3?HPjV78KQ8Rm zVqmFdy2zp1XXHRJ7uPsecyOO_c+?m+MM}8vP%cbyCWCxJ+Tsd&O$sxL+s<<#&SCK6 zqZ3Pr44x>TRhuiF5czEn9wL&Z*&$(xE>`0oKPiZO#CVDSn;F?{YA1=?^@Gp2nFOi{ zvMCDSy<20}M&Fa2i-8GjN-4!p&!=6|h z3YjaId>Ab?6(qevQGb-&G0QKx7TL~zA`N7?lNFRQ(~T#^UcDo}!kOvJaXOtG!5(#G za><>&=P6&i^v>UsHm1@#={lMSq&mFcwe7!5_A2@dz01K?npDrv@4CBE5R?MpZ>!t4 zZx%arGq`(j)_ffO*N(=mlL2D2PZyOcud>O!zpy~wz@0b12{Q1&t~?tw6C0d+EBc<3 zmmAPmSaUw#(n-#5M=njNE4h}P3g>kLyir@D!S81Ct!eQ54;ofpq>`F75 zN_LvhRf%D~7Vet3m8i}?hSIR6!)@5e5tNEqS+NiEHy9;lEoJNro0>EZFZ+QFMx;Jb z9fMNVvV_Tv!sc-Cp8;P+pCOBW%T%fER?#F7FRibVby#_Fsd0-yu^<>ndRbo5R}fD@ ztUNbgg~6i-nXO|HqdP2B-x&WflXpdWyVu|VKsn`qnaTeG>H3$ni=^oT0RO4~!PF|7 zIa?dp=$l(PGC2JgNpELk8ZIv@1_zD(AM7QZgt&+z008`t1p0&o`S*&(S&jYI@hvZ{ zED8z=3I&S-hlGI&3xxrThKU4=j*bovLk5RVf(lQBj!lM%%Su2~tO^i)WK}kl#O3lp4LQBd^$i>M_!_L8uh#>@z?~TVKK}jn@ z$u361rNToj^ov`Vja!M9LytjFjz&<8OGtu4T!l^ELI@XDmK<4_jm44$!-bzvmP%5K z)!2kZ-IB{jQixMgoYzZ`NL*Y@SXxC=LH&=6kh-FTs;a7}q~UK>6JbqfWlduh4JR=} z4-ErzEkjp*RcRwFC3hhYO9M3rGi_5-QzJ`fTYDP|2RCOcBM(P&H+MG#LO)dUKzyc1 z{9kcIbiw4mA}DwgS$HCu`Qm7VQu)LJ*?y;RX$5d8=CI4<@u(E=>W2$U`-?~=ODYA4 zD`u)_2P>;(if9xnY3GZYg#ETkl61(@HVXV>k*(|)rRtt4X;32LQmAH9reI#D=o?cm11mOZ0=lX>eb=wTIK51;N$15?U$tyRO1m4 z?iHM98dz)|+-jQG=^Rq#8r0z(->DNjYLzr=oxA5)vg+A*708PdCybXWMOycVvRj`q zRMb7l)2`UXbV!?e$&BgAjdRCY^ugZ(ZS^%rf=7=wSDbNUx1lp9eqLrwpg? z@UY;J%;>n7$b_`G=!m5F=;Y+&ps>P_(3YswqOjD4_?Y(8%+mObrkuRY?EK=)!lt5( z*s`Lm!otF$ik8ZTj)v-@uGXsd_BOwm{?OFXl+2-jS0OP43rR&IX=Mw!4I6>oFZo&h zrDfwq)$`R2!{yCu_0^NjEfXyrD`lN~L49wXeM6o7o3;Ii?W0G1@d1OKjW5-4uU%#R z-Am&G9rL4IPb00v!^1ywM?i&G=>Q`@Txlk4m2Ba;V{OJ|cCcPneB3)^?Qd%K(a z7wgB*&&%Wc+o$&jn+{v=-Oc0U{q@h!&%dwW@87>|yx(^KfX*KY5kY0Q zjq7et0Zo-2h8`vGnJ=zl#~~yuvR*UZndO!h{8@1bP0Z~23+1Ae?nP1omL|4o0N-z> zbuP^83dlS>ECK?oS9+TekT#4f4tpv4`nI3;2Ou-0wZVx993*_sedxRO%c85)BAnERON$o-BNMTF@^a zQI|m(^bM*`nXMe@$TJQgsNzS~$|XZitPFi;CxZ!$rsqrrKte&v!NkS^sPm6Sulr?% zf~u)N$AuA2%xRso$A2a!9{Qa4{p(}%!xt1X%z^XXGy|4OhKsdJ61I+CADzz7BpF{d}1Nl>i63d)%mM zJ20YfsSPObT8ll$*tX#l`$E~Hs@!{R1GQcYFzoY~yJaq5==aR~#t4Desw6UKjlxMI z7UNW>$KxAQ{VDgO(zgkcVGb=Z(8a4f>`_1CPR1XAf!STsDGsiSx6+kf&Z;Zr-3L~z zIPWe$h%kcW$=68%Y<<|fbIg(FXrw3)Yv8O7>+0iX$)wi=F(WYI)O1KMalHM~Uh=4f z*F=#*_q0iyK|L&tcDtkfOXCD7=irUw-9k5CR{wyPURC&O^w4+Xozcy$m)c0}qfzeJ z?^wiiOD!OeCkHts#O~VL z3$FG^z{)`w>FabJstjsFJh9(KF@UEr%C)$wor=G*QP}ULxv5a>V$NjUsn%+0&fnPC zMdB^H`aL2rdxTujvNXzd&V>wx1(F4II%ukZ(WDUUcVM2tJn3wbD^_s(2zvfH@%hk^ zHU_9edp%@!3#3N1ySnza5F)JzOov7tu%Qyt%90AD1v{jhr6|xAznSB9(cT!qB2K%0 z+**5OY4D_Ga?Q!~q{&Wiz-`XTYTUkQ5-oIk#Kx+_SR;kS7sAR?NYx;>F9Kt=D9h~XXv9^ijDZP@8StoC-NT z8U$F_*4EWufpy+|I;SZtT6_CUM73w$Y>3|J1sZl_Cq^lz;P6C8^do4U4tqA3NSsHX zd4C9@ywhI=YV7rn=v!W;_)>0rm2R}y8@k^qN06u?!jATp*KhOfsAioZ&7iI;_zeX!SKl2UrNPV1HenI9K|*2E{-lEpXt# zoDb-zop4S&jU4%9Mdwu*@`@I|Wz# zyNc_johA2%1yjc5KVNu!>U^n(XDa7K#w14DCRronas)Qm8!yB^3&@E}iEVCc=8}dG z6fE^InpjJZ(v~h&eX?}eCEN;g8VN<_WFYd`D-?5-oaH@+c1!8pMMac23fL>cN>ZR@ z$&b3vgt=@qMmc(55jIr_aC5F7ks~U9O}x@}vQ{xp)@k>B!cVxnP*m+z zuyl3M9(2mGUm_A3+hBG2?+uG?U$nxW1;eRYoh9}!W%xqio zH`fMQVI08W6Y*rQ&_y8#d21`{Cf7LQ{7q#gi%j}OA~_f3hggzcqnk}OHOV(JL#Xb} z>W#3W5jL2($>`is@Vr5^2`O7zHBe7(df@X_^r!0X(OZDnZdFFSB)Q5L$V>VpW4p=h z27<&ivp~;D4{oUFS5R7NS2?L)xW%*bLY*nLblNFQsI82kLGAlsQ1(gfMh(#XFc&(b z`~JwQXfD!@)IJpb;VgrC7&_U3U!oVdMmA_uH8IDVytz(V4WAxyvC@d!%vmO4GfZKX z600>pR!$zJyGC(8nK_@Lb+q+SLfWuml0&^`z>WrXqz+Te^A4?}&pS8CXKLh>Gr5^W z(R`c=ZHR*KjMh%mZ>uC}6=n@#!$9|=1`J7;7>wwUtZZ;+dD!RGX4{r0rh7&X4fKo|s73G_k6H61K7b(;qu%gO<#H(d+prM)QdFe4g z2YZDJghd3KV~#~jh?L3uL4gt1s}V{bmg*z(q0xvSK$j)v6X(P+AR1x`QBz8yguygu zqVWiVhZKX)LAM$A!nsD-$Zj2##qQs@5SLhnIj{b)86GqqBSAw*G>#q025qOt$B)8{ zjaZ@>8D)K{jjV>o0Y454CHuWA#Wxtu`A#>A+R4oZtXl-k5dy9ay;twye>~!3c+Y6T zQW+nl;O_OQQTEj*FU3AqRo(;T^MRF48JiSXg6d55N=2m<3svt$fQ_Dbw>A>E>G-%qTYkHghBqhlM)erwJ5zk7Mxk*`h4wi&Ra#;5itQjdKF4`j zN7LG$KD}A0odO?}-gNf`AtqaeUUu7_NRvCBz<$6SFV;BGKhBC|bHESfV@Yi?O8X>kYvOC;Z_kDLB=)&r(g6Ny zzy96Y^_o4~FB(6^?AYpXPoTC2MX7|238DmfRrT?E>coV(=(xk^qK=d-A?YF)&?D^S zL+SXGc>FHDC2U!!7;5pps(v}+TF41ku1 zvA{aI%Z7Vs5Sa?upM{!#X|$tx9v+RIK@Db zR;aw7X*t#ctKt5kEt+YU&t(KT!ud}>`^FC;9cF=epYsu9S>>Fy~s1Td>-0X-BGe(5c8PlYEPX4hPyr3p0F{2 zD*ha`1MK?eV3&Py-8Hn$3-IB|vIJ1GmkylVdy8B22K&yT6waGgJ&W}W694^3_`cGP zl4Vqfy4U-2$9b!-s=G0y+p3@cRoH2asw7`aRfE*JG(U z&TISrMDOuQn&0^cXt`V#n=}6U;a-AL?!v zkrbliOj3h#u&5mSRShUvE(Xk~%tNa!e*-KN0r^&7OI7mjRbjwcTsY{}yH3DFezaR~&5efCc-fn%r4tuw62>#}(?)Cl^6aq`TS80X|wPgjn`!(vy zaH3kS9se+=O18QoB<+V({J?k%t^~;KMGY|fQ$V1nFh`u9NfEQ`ds;Rj1CZW(X1v$; z4@Fl*J$^K;2c5KoBR_$u;~b4Kz~Ymecsn|b3@*S5GzhE)fMN);?frAIA>olX+z-O2 z7UL9;i3Jk1!(=ee1rqZZ79U9ygzf5Ax0n)v(y~^imG(Gn*jlHGiNATYXXe%fc$Q}#`-&9BG#U?Vk4 zgWYs4NJL2D^}DVucMeh7LhaIXWLM1M(e5!RGs+6CH-NWG{938oDJCZu!v_;l8`V(4 z%1KHjXXj%W?s{^=(!BCm9QCsT&jmxQ}<$!WhBH&P7fIw5XU zXCow-nZ}TYnNG(t6tgnBIj)6%4`M`-p>VK%e>doqCuBy&BNcJEA|x6DE2!^T+5(yY zKO0(ffN&e}66VaMoJfW|HVMwlvs4S33~`si3;E~lKJ&Z>ITg&5ja~nyFa~w0=*%No zP0D0wZpRNxUj?VsS?V?PQh=^g{00#Dn4|i8QUM4B5}7Osw-*+oWm=w$P1G12KSgwY z)M(<_^jYCy$Td8I!3#NS`%L;4uYmot`L#n`;0)W{Y;C!J$%xccFQ-^giZQsnN@~Du z=VfVvFIYp2YS1cLgoF)pgvgr6I$?(%<8?`dw|8Pt`pqpE@L0i=p~baP0@Kz&s}X@g zK6@Tg;`9{)FbPonQv3v>@&j0jLZB3+rDf3jAbjc}4Ta2w@H5B}3kVg&jXx{mLW{$L zgor|*9U+2*k`S09f_P|Eg6@M1?9ijQVi*KOF@NO)Mlkfv2|)|`WeEYM#Wg|nnZUG? zDykI(0E%a#U%|ht3UW%;V#eIW2G(BHRG=S~| ziiOjDs4_^_j!Iv}Eu=;i@ZQ9PTQrq2wB31)uSV!nj$U{V~oA1kWsNgvl zwjR8W*Z*oMVW}yZCb~}pXr>Rtl=yY?a=aH53{xO3KQUPW(m-qCFMkoXfGSYNqp#!P z(D{V4A*=$FDwWa=H8n|q21H!fnwZGUE-*7eHDd*F)CJVa*(aed?Eg)@vlQUk zN6U!+@}mF&zRb^xAh!b%KsbwecWi?<^+!!1Q~+&*)^|DpI!MRT8@JQ>iKE>)1gY|* zv6<-un1dR%{twpqoVY>PI6glCQs{CZoXbjp|8UN>0ZaiZ^3RZlVih$7_hO*)ux!W_ zIvZfhv~(mQNTT0_k?T4QF^7#eLji`g^`TaP(EyDy$4@vh5y;WSnQ(;L^cxLff3S(3 zo54g+13){lSm~g3%7inq%tED6DOT4s0iJ0C;<+*LbXN8lD|;e3K+Qa1H8IKb32yCJ3!#Xf&@sUXL!-8LaqR&5E2$1NcUat)Z@Zm=v`YpT< zy`uf(aVPtvfWV)Co1p+cvxLo81rRmKZv<@|r$etdk!umo&?o>wS*`lovann4-#NSZ zcYgG)r(}2B8g~DCGcx?)$Mo)BKl-C5-_Gyfdg}*&{P6qkxc|CN1$b(!_Q`Ed6Rm_1EUZp=3xEKzSgF7Voh4{9 z`4m8pll_RE<4{MM5GyfKzmddEPwpNX!cCRjhsnW|=yNxL0yw@6jRFwV+}5-L+<*Uj zw|;QT2e<6F|IhdDy!FFdciazT{N4NC+j;-}{|btJ$Gabaa^Cr`AAxxAPX`15x!&9P zhx>Qjf6Jf3-uwOsJMX{s$M4+_;z6E|XaxXu!Yve8*j9it(0+g?V^ZR7p9)Lb6G=Xh znE3Fu@RPCUv>U2!AHBWynQenyW!zggfdYi63P^OY&A<;o{PTw&{h6}gKV-~!@DFG0 z{trKN8TlWw3myGuHi35RLGG13lF8yv0p@K6>T4T7<4t91B%K;90jL9x8B&^nM?;W9 zwb%KVUKs@-dWV;Up3B2Q!9oh)q^p41+5G^2_|vMm7Z3!ji~<;STHRcWw{tIQ?>y=m ziPyDwpZvTdzs^}pfrHT>kRM=piD_;FLi z!iA$n)M{--iY}${Kzy<82UYtt^~t*A_~Nqgy=9fUr1%uwlg071WfAInuDT{6QCX9m zm{b!LuiY14Ta%>SR~!`|009C}fGJahCgchblqz5l+0EVH)hXZ{b@}RDQ;I&0Y^qG+ z{5sSG7}9?p2m!cT6YC?i(Ul1m>(tSeQMF~8)K64I)P&dW+It|jKB_Wm?-SgF-Bi}7 z;(ePAY^VsYj!JoQYgvpoHZC5-1wjGmegNGXy%+E`$3Y(0L?m$U!K-)2ZRi<|BvZKD z9^wIR{5?#LFOctQBa)(!ju2abL;Cajd5T5P5}>BOUKgDlQ@9dJsZ@~d*k8{ zY^4aW_rUE364lWMBDK+bwYvGvPx%&90D|rbT;}lrxgj)2~~81fT#;iU68g4Lu08UZbR5 zHKm~jm3m%0MT7NHA#RM)=fzY?CPBl#CW}3k$*gt#)qNFUG#GCrGbJY{0~=7iuafAm zCAF%l6ml>r#YR>qX)2iqtZ*7x^u@a#cXT|8if}qVIPv3mJj*cmQSq z>Dc(#m}z|G2vk3VP>0qd2uHuba8&j!;lzE`Lx3^!&BotMvPQ zm*p+<>r()F81t0ZOUz8v8_lSZr}V;ZT#nuhRi<#O^fuoN^t-TpL~)S zlR_GKO%4evdR4@+%U$cN<hTt8q96Wb zr{edieBj|zGyZ_B0PHXkV24P+%fjd&Na9@_Q1o?x^I_D9QNEd-ft2^VfcgPeMgc-* z1df6>Yv_%@9{m?;Wvz<(3ffN z>}@y7a>lT}?7lo~vcSe`RiL?@_PpMK&atKom{^2kt#u&o%ep27_Kn0Qm>lTrz{G?C zo0J@}i3maKm;o1C4S87Kr7~~0>yY2+xtk; z(%jDDiEtDUppMrH2!Qq#4WpA1P|0KVLUc0quq_SH1E`Cji?m?{Bq^Crot9wsY#a7v z`NfVV8=5=7lXpx@P)CLhTV4eqVi#4dnpVaJAWexOFMnDAG}Uee=*5gegGAPA6wA%o z9cXK&*w|Z#8H^o;M|#Zz;*4ISOwct@hjsS06w32?^86gLMC3SxUNM`^c}8J#-ekvc z-oaj;@XA1uu&b*>&}uZM8dD{`z4@X6RG#0QhswIK*4|E`)YzMknd{Ji6<|>|Ko5c_ zmudqn=3am)*_6n*u>@*6k~p4d@^Pc?Et5uVN4cIf|0|Vi<~p>5#v|bXHUOvsK268G zX9WbNJ#Y53ei$|NAJ1#fK-z-|FBqAFXiQ3&1UfI~YAwu|Cvf&Gg3D$DBrj%);ib0% z2qmszoz|+gs&TD5%sM+2(*PWm=GrzeM}R#O0>^sowSHYgie~Wya5$-sAa!+hdH_>+ z&GkxvZ11-KNCiGj3Uqp~02ClO@YlzC;m;rX1Ot17VQR_!0NUN5lkp|ZDB<+20#*zK z3oAfUzy$b@rM@+6bjkMuh|$myAVs-^DuAG?0AK`SD?348P!(XwZw<35mwGQClnStv zIfWdo5m*(d0$?aMN0iSCVO#;sD`G|%^Ep=lHo(3hyf$u83b2@a0rv3_Du8N9RX~qk zHKPhR^A3ZvGgfBjN_FF`Pi6*Ze9}CgGu~kkIO9@)!i5!Jf1u^?A4^sMTTpBO`!}J$ z(rf^x8K|!EQGn96wo>3`f{nY%`rJ!fn^)#iCh_W3S9Y+w3LKks)t!S4xUTuOK6nZ+ z-#n0k-B21$T5>A@*Z>#ZQ?Lfj%~s@e@XuI^4Zt)5t2_!I5DG!j(3n90Hty;SFiAd{ zywg11tA5}nfPCR9AbHUpa@Tzu(W+T$PavfL7XvYVnCbQdb+aLIEJ*>_W?+@4AK;g- z{j$_=SuG1(1mL~Ba1{`6E8rjVATTbBCoT0}fPK_zY7NCS>>{85nkCr)Gx`Bmh5%?$ z5EUQ@tpN4X6+o-}T`1a52bZP*Of%4J1oBf;UkoJyd~JYWDZtVRuyh;1KGdLoI}`+< zY=9-S0vv85kgft!CC6S|^9tY$f&yrkcrSp^CTTQsR~5jQ3H;Xvhh;LcCq5d@!Y^lejFL@E*omXEe^)0Xy1!tf4i73fsGnclVe#NI~ z+g?8730(CG_)~zO+5oFS0q}9XR^?WJW_uo`uJi~Q9tu`W1Q-F<*5PJLIUOs+glKvy z?d1z5B!=OpCMoseE*@XV97<&s;^&z7FOT3P~1uB7d1>95|ox^3NwtEHfsSGw)Q%Xb6M)e@@B_fA{s z?eClgM}kv<+;o6B48**8+K&PR&=HgrXfu#8_x`un;tu{JFaqG3CH4d04apjn+Xnb1 zZy1%CuP8*59m9gYc5~`PORwN!N3Wz+!4nf1W?>-y3{k~GpmJ>hR_*`4*`j_v!`ttVD$X6ondwKnR>)zS6 zed9aZ?%H(c%gFQFUU+Harsq#>-13W;kr(cL<)w}H{*SZkwnv;fyM5!cr85;EfR3P~ z>uJq7eFN9o5cE}mMHqoV0UF#!V3sYH5MC+7Qkyz>qAbENnQ!UN$GUp!1g#3}Akl2; zu!{*n6DaN`%LTlp9i5aIEFFeaY+@KQXJevPbEjR5WAcvHIuR}#Lz_BsFQeZ`c)_p% zfCPB;1MpI%KmptYxLWeq#*HQa)b_};+ka6SafVa++})2ndt%F@k=r?EIi-)>dG_kn zHzQwISNi;YSJ$82c;?N!+agaO_icQ3+p{I>O1@jV?S=JcN=r(%|M1R|^`-Z|9C4!@8(OJy0K}H#1~-ViLtc4JaJ)Dhd?SK1kLTu3XwI( zg0?DhyLl55`lsV9y%oUr1F#Apeer2G0nTnOEm?P(Q+n?&9@$AI;ibDv!_VG+=HmF_dfU1 z#*%ey_r7xbi4xA~?_PcQ`Ik06TGBRC0Rk8lnv|$a)CB0%iJ%>}TN|(4?Gx=efpV|% zNfo`N*4iXrr>k7zy#Rbf?=b?6sc1%rsMjdNI(zHTrrr*r!Pw1jGImK$nau_PAH?#F zSZK<@BSa_ilN7 zwC(L*l-$03%R7&5IkElfwzDs6y07H16Qx^9xBTL<6Q{RqIrH+y=k7dx?=K!bac0Y= zS4!@?`rL^UPyb*Wr2stF?cjyR1e$OY0an+R+51~@liihL++@;u9}<|e;!CvwycNLj zU<&qqo1TF&Jd7Pa!J9F5hQ4(svSEIQXYhj53aFu*fi4B`Gy~h{1y}(qXLXCRRI=L1&j=8G^cnW>Iq!Zy#R-|0*EBX{1ITeyMlSkssQ6FqX246 zA#e370H^|h5vX)`1RayUwi5ROc&l0g7Hb21p0m8j%E`&iJ-g&q06i?+)fFsV5d{DQ zSj`HshVFH0^u>Py0rCGO?Rk z2|k&>{L2XUtX4xo7!l;Y8+c3#9ED#}?w7}0F7wzg zrEUj8na6;~y`AlRvd&1LPIc^p3r!@^T30{lYb zJx}=ZLhHI0P|&|j3P3Dn+MOlL4}d3`lE%U6&w0m>(V@{~+9m0W^~HlT*OZ}@L3-=g z6%V~Jx}++=`@s+%4;7}SYWjWVb6-v(XfXCB$n`b@9p=C=X68OTYz`Q~>Iqyka5!f? z7{WVr^r%|z`?7##R)9q{1Bd_eNnq}ihaUP%pn3oDu{}ul0@e6=XfT&)21!4QluJoMO6P2i=d?_cOu6Z6kKs&(BA zq7=XxFadB=$8{8I;P9^irW;rRT&)1M3J?f|M~@xV=)FqH|NO9OuO#(6EKx5oU9<*U zR7a3r^V`2H3jr?adj^5i4>1m3?7e`sR0Z^SJs5%k0stFeX1UXWeH*m8nD~CGT$6M^ z@Vo;^9bO-!^;^c;XLJc_MkVH&eO&APaHu120w|Y>0Dg=5hE@RI=K@#-@cXad^PQBM z@OyMm#8p1QNr_I3tJIY}kf^PXOAJ?Xw224es`k}@@_sTdNvGZwudI)YSMP}^PS8}x z#n-D7pN!Y~oiW<20B;9w0}wdATnaGL1|V;EBhWP>h;0U9Gyy!V0Po`36TkI|IPO#2 z>Zr09ZFJI8n`$D|yLLaYw}Mm8(MBabx#_9+nhpE7+US!HBvmAp702xb`J!sVtDf0Z zA66BcKK}S)-@<(GFP}X0Z|{3WELH(-WFydbL=dY0Y9GON zkzh|!^ycKPPsAi9Rz&Ket14p3Vsy{k6J4wh0|Wr!San3g-aXVCE9c|e?)Xu}&q2h; zfBEG7zkG7XLremb>64F{d_MS}fB^a=?#)#Nc#jCegsDfp`vJWFvwM>Fa4WbK8=i`< zh^V9pppDuaec*}M1X=-VDmLk2IqHfHNm0rtHpRt9@7}zrETSe{xp{YNVtoXlk6&ZZ zZ3E=%mZkupu%mt+b=$v1J-FeMJMQ?-$3KdA5P6^T3E;#X4}Bc=@pm4)?ftMjezg9M z+dhf9<2$$AcE|e>58m-H=e8LP_@q540zqO30H@m#gEz1dIAcT*s{npY|9UlmQdeK5 z)m12KwAy-|x}ru0{MFiOUA0zQqf>)meHk$Osw+U67C1G?2Qq;S^-8~Qi*YN!slZzS zehKi&9q<3kC;$BO_c?cb=b;}x_??HMK(Rmc9V+FHf1?O+$3vgo_VcKpe-ifZ|NQR{ zKKTB3ANs9v>QSxG6e+fw*{ma=C7|^qU z^Ah0w+aCOvkE8zm{qKD8{`;Jd|M{WY{_XuRK!J}R{5MeCoR8ms|DXRo>gTsnT%ZZ? z|NQ7*KKc2_-}%Ik4PYu>^nIM(H(qOZthp+H9T9|iwE_Zo2T+h+9<``efL|Nnq4(ea z%Y*;^mj`eA7f=QK=ng=L+dg^!j(>jellP;5*xv!7e{|dXx83&8U+(z$p@%-EV*c&p zJ3a}h6%Z&M-<`Dg$=k=)6ajok1hK6Et)IVre=u}lc87qw6(H9wwH4q~kPF!Sb0z-I zrPTI+MS$(ogM4poX@Jj&AU_Ha(EGj?T~p{?3c$-$M}O={N-W+giLA zQ03hU(9A5#fa-z%>z4yvzuMafBvzaNA2%?4z^>k4X7ANEJdbPOAP7ebNbpmi-sJ0a zD|Iix-3kyeRe-*K{*vtv{OzKyM86%v+Us~ce102X8G8Z)tWDQ}vQGd6`0EnNV<_}h zfNMN}*S&yRjYh3uR%Yi8(Qnvo(E54%u9|tDFB{e1$?28Z>n**mnDfEv2UvLmkUv9^ zFQR}7EX4}cuRs;xJtBy*0UkR#1BZOPbm-8rqlXRw@1aA-z(Obe!jp-KICNiN_-BNHRQOi*};V~wB^r$PA z^-_53*o=5OGjlw1(xV;%Tz5afx&O(aoLb76-6G*WMSFYul6w8EkNnfRyV{pj9{Lri z0=zqd1auWJFF^VI3~V2zcSmX8F*?XZ(7vCs9HEjPW%dkFn4o)ud0r`Uz5M_x_C4@Q zu&9;ldNuITm6ckhSr|$+m6NI0luG&xAj-D%pL405#zQMm1u!Fme)o3%+i$=9yWjrq zcc1?5)4wtc0K0wbchvGJ9q~Jm32ff_>)Uj;x5}v_fDgpcJ`hW#07o4{EpL^7`a72c z$1(Ar{_eML|Mu-daM8lw{OxjmfomxpA+B*^Iifjk7E5O!&7eU;ZoB3aGBBu4T%< zrmDs-sC2JjZ4Eet_EmWwQZ*~a3srP$htrs%6Ryz< z&Sdmpirr56#!cZ=W+!9 z@0Hh+p^-V6+Xv2bxnJMYKle9B$eRc&&Ge8S>FHbr*68D5p>su3i>|Ssj_`J~ z4rH@>qG%%M=qUPHfKfXA<-wjFNPrb<1$flZMlW8t=GxLJwwWounLP*02-tkCfcrc0 zTY`WDSgBS3!3=;RoK7dj2YL_`Ga|~pbJ8BC!!<^V1smhz4Hav#U0Gafn%o6KH_ z%|72pgbJ{{MgU2GY5;7&jwGC<%drt&j-GNGEQD98bA}^kJLk-dt_W8ooedITrEGxJ z52yf;08jxS0iXgv0zd_T1b_+v32-B$DxRB&!xF!F37|oc7+BypF#(9MB}{1t32*}- z9vOuNelrt5wK6q;eR>sP^%KDUWx=U}GX=}ADR}jNEZ3>e$S=h=U7k(uoL}k7)k6Su^%LMCx>oUx|1V@RH9-OdZm641tyu!}`Ts^5Wj-W8 zz!>i-;N4&ZP^$Jr$icp#_dS>~Io77Qfl%R5B3))my*d$SObz<^P#X-xk6vP-6SW}TP4|AA#U3N)!S3A~R zWXuM+i_oS{MIP4NnU|q3@N$e6foOWHlgKOREJs`GWEP9C$)2Cq*^&08J$q7Nlw!@s za!~x8-T6jgi=r!SVk}MGS%ezuvZ-^)I$KbAT~}c)Q7-Q6&K9+FdcBqjy^zd7l4cV? z&DsEcd6==h)!5Z>#N5=KMbz=G9nMY#j0z+tuwqZbRahL79j^3-o6 zs3qHc`O89cyF4#v7@a(nHTv)FAr?L$;`sV(kweTW5IB4^MGvZ;B=B3yAWHPha$j60-a=tysCb*z5l=BRQO-4bwT{xZDsu1B> zHi1C_exL~nIK1v+o?X(VDCpo#=HVcNIX~AX8|KS0EX?n^+N*$`p0~Yz(yTG?lSwB# z`r30X5wzqL@_YXEVXlak{DQj5YOGI#V+ z3Sh-r%A1A(os8gLcn~BA%`L{Xu3p2~K_aio*l9d$STVcVPj76e$ zwi)PH-3l-`!~3WTU2OF&ucB7qoKLE==|Y2e%v&~8|;6+kI~>TRa4kjL~xVyIV;evgpHxSbI| zJ^h!y%@!(~cV^~nJf8*>bG3r5u6CZ+DOP6G6L?)l;4B4L;{@>70ILHztGx;s zynwAy2Jr23*Z|76p#oe-70^?Ft;zB~=yyN>tpLBzIzl;mleHj@xEAl2>%kG<#T&vQ40>IzE+U&J&aj!ESyZGdpYiar0jKdakK1;LLc1HY#F8}ja;WnTEN~H=o z4R9*8O1Y3@a%sF`;wzP^>u{P`TbI+W{vlze9-sk(`W}5xj~@8Ij@p9=deu{#834&_ z3bVK(n9~gUEIoRa-siA|;5s?GWzOjSc<%Tf{pcJMpil}x&9&xL_?mfFjZZn!Yb6q| zQYtm9qrG!(G+w9i-GWusum;8inoOf3<0Ipyans27IJFxepY+mwllm;#_f zo;|YzTiirX&IP=q5eSk(N3!|m+u$z0jhfAiiDg1I(;Tw|+*AZ`hI&{y;htv;Y^_u7 znX{&@(OcV;_YSvt^5X35TsS_Ol9ZyHVv=lAu6eg`+Z-F$S-fWJV#6dP05CE`eL=!O zIH)N?oWMhE=y_foP6spT3rMG8Np><9lHM?J%{K`Mn*U6i)`Gp zDFx}#uCI!z8buO&!iJT}NJIUm+8ArX)?(z6lO$r#F$7qNPkMY6%@6VA4MTuyi0aAR zWZXG2x>3D}-17z*dp==5S$)qS$4-O~k^6R&yJ8Zf8p3;s=tj~Rdy+XS^b3vFE}1Sw%!bbVEPq+@6zUtZH;aVoc-L6ajj|W8y#U-;j8_{i&F} zVVB6kNOGkVU|gj<0tv88y<<2|j@qXjkodkPg6epcF>gLIm3AFg9#(~K|v^SaihOq*3U!7>OV>+|1CG8;AWb75^nEQ%yJJ3E!pE18L-)?Lz$0o9f zRG}b!Ol;1w8myVe&Bol`UfGf1E(O1@(4dx+oyZh5n=N9Z zxmVGbCy?ZJ^CqMd*}Mr!UuMQw&cH#(aIXR_Ao30pSW`#tFwfj=7&vYz&k$J^G7&B$ z1X-5c0d!Jg@9mtf6E_)#({apDG?{tWcGw~!6y`#^d+rmc0HN)z#7#pQj{*!d*{MhP z)1;zlA*ceTQ>P_j91!4}%w{3hS7^_aq+KZ#=H(7x(~=1ZHX$)*qJ6!f+yy|=n=K09 zX==~y#`=mR624S!6G(Hd*|vj(FlQ{EnjTk6{y-q1Pzr>yQI&7vv82<%uRb%~H8#*eEC%?vrFuF_!K=Nw0+0 zCutrK^AcD0C%bQK`)9nQ{e0of-~(dP03bIXKm z7**uwjv0o<188$^uW(WT39w=|fJP;q(HVpJOk4`K)Z{JH6OkCtHR~vkedHe#chz}W z^>)m4EPVtI!+6XI-7^F5sI#$8SNeO%^5!OhzIbnCNQVSDC?jxIKLF3q^Zs+S^29Q) zV#M;i^YIolcfw6h00iNLbU*-Q11Oi)6Uc)dK|xex4iX>wW}xN>5B3DE5+oqn5cgM7 z{Qy$f6}(!YiXcL)0H6xc91r+zy=z%Eq#JH-0^rFj&redxnO3z=mFj^z=H|Gf1tzTPg#USx+xiB)d@|NHKkmS%(C))wBCilkFxafF3jf zL(2d_02BL!K0PCV38$a?rykh%>*$4=2*BhDQkz5si7}bxJ}NMW&ovwpxq1Ho)OIwa z449%G@dvAk+EP2{tTV`%4LYVa|P%6F~3&0fyGz1ES0NiEj=940-(lMoo|atBL@lz(~~( zEvr-d2;iF20Q|-#Ku;(u016|J0IP}sp%|s1_v!;4Uvm=$-QZO~NV?}d8Zff<-XLy* z3J^RE7+-_4#1K#cg24d$8vXx61qfm-up<9|y!+D9aQ;$I;JCRJfT{&XSH%7^>lTJq zt<|~%)mpCr*^s_(W7p zjE4dho|LDQ3ll(Nzu6UFWhMwAOqa9^7LGbhqtlfo>YgZmip$mh;M^0%+F0!#ZemS1 zw=7W`$BoxL@MKJJJx8lMP+P|3R#qf%AYANKACRIzk<#d?qEjIJzn>Sskw4SH@9qE-u@&Z>ui4I4b49?PW2vSSZV4 zo{3RM)b2^F*iam$+!d9ii>g|~3J^L1^sEE{Mh4F>TsZY%YMoXWo3Ke6o*bP52#{1C zUcA06Ouf0XjGM4q7n>Y)t|GQ3T%#)ktf<&r2w&; z)CZ7aZFv0VtcY4;%-hlz|}=pJsDFS z9nYN^_?UaVesYuhBgB2mwvg_+U8RXfezTOTT#HIfj9A#b{|%?I zsyghh5Kk3JIL7towfcpP006M1JX+ubjRG|lr=>jo+Ip?dol0$VS}FoWQ;WKv%C?Z1 zsne@hfB=t>-)??ul*`>u#wBmQ#}U_n@8Rwy`eWiZGqZM)uOpfmM2{fDOSsA)X9U#l zdsO5Dm!=-z?sn*6V@{G4TyD=Gmz(IQ;l|RF2o7_$l_9_0gS;480yu#J{7tu9=%2Bl zH|I(yKq5)*`)R^Y_2DC%_v2xgV^tNK^xWF`{X{fNfb~dY7;-5J`SnsR^HfCS(*!kR zT;o%ZkYyvAKf_Z-5lzay2Ob`6Bx2tfrHzc=A`zNIO%$@;5n2Ioj_A)XY@siq@B@Zs zMHGOH`*m!5Wu+xnMMhnU{o=swm6biQ=cxJJC^){0Jglu8jY595q9X6%Jmv@yK-yo1IRz&p;iF>->$1T`~1kffW zyOOv#D0q&{h1BW-T3zg@du8;krk)Xjjv6u@t>da8&1*`&Dcx?^?r1H6W; zk3iO63c-~wf0q-*L0$`SXCP$+Qmp{}{0rl}{W@k@(Dmy*2hZbHZ%2^vwQaw(hU7}t z-~Qya%c1TEcCZQ{6QzsNmj!{H_S7ow2^a)!g@3NGQa6GE7Gk-*@U6};KT(Z_SJ>HG|Qpm=B|!( zs{xl@Ea%C_FmvaGB-d=bP(YNs2%v|zgIDBkAk`1>g|fPuohJq(nu`Ydr1p;9b|X*# ztWRdl$0iGGyw*A_zYEXnGj@(NwevDWcC59oRe^q4*QCI{kytS6a90O**xt_j=GYON z$S!CdyMT))FXUn7OGSd|)NeBRgcQdN-fW!8}KB-ljk$NRBYnW%uyI79dWuWL3@@%xZ{YrVNMBa_b(+RAruC)&EqDg@^ z^|tdSGcf_0YaYNRGqWrr9Bb+jO8L@kt5}+4%%4s*|4L|=S^;UgiiFb=z^qoqM5c%k z+g!5*uU-NqhjXIrA-NJ>yf;M~dIcEKpVw4Zxd>p!`heIyV9OS1AFsNT{anA~qn1GSo&O z)e&@F?Ir*-FQo7MZS*au;2p)O`7;ikUuBkliQ#3P+YQ~&^^!%Uhi6Hsee6Q>|7|-u4g1q^s7+l3h zU>&LyeMal4_G{ znU!@)*SeS8KB-F7u&y?u((U(kT58=r!=(hF%|ML^YSd_|ycA$%5E25Iw8W?%H}dU> zCuEFJS&HC$o%;UN&03tC61aL4mfNvG> zPHE`iVt_0p3J}8QF7|9$0i5IB%|N;uf2Zwg$bfkiVEGA9RT)ABurBvp zFF{cN$_Q*!c@sc*=+HY?SAYOe0aio-2)Z8t*Z?j9@Q%Iq;))O;6wN@&2AH@K0`##q z!15}2gtdhrG(KrK)J zc_;{QoFp|~Hh@#F@zDZ;9N+7&k=mf2w@4584oo2S1;qv+yleoT;FxrU2p|iE5opqn z4&{b|09if?Fco!AV#Il`iB$IWI86@l+j}trpe!OWF4FD^6F#W~^EO)MZ_DgX2FD2W zwgLFZq_02iA;77hY(L{$UZ=O8KIR+oXer|zN?lWHVJyPk+n-msf|pn=@nGtQ0sDV6PEk_fxB zYxg~-ST1*DoEy8_p^oLAb0%=PjV5j!*BYnNZAyqZ??{MwGSWdQKoEwH&^>|fW*|=} zIrh?Puek_N$~p7qeQl?%o-H|h_SKTBr_a7x`sSS_XRemKTGDpvbm`TzkG^vChwERt zT5|U4>5@&)y?pnnGgsT*E&zcO@04tM{`5O-r%!-fZ@zOj;>>Aq2nd`idA0Pp=Stk= z-xks;U`(e}9>+rPhMXflt$>85sqIsbkOvyH+PwXZ4{q%{aiAqz%8?_Op?|d<6Qpfb@?Xh2I*ICQ%{hDJ}M^I&d zG8jgns};cX1cEByH7^3hYc{ zr;0AjZv#B@FlB%4*-zGN`kF)PBFX5U!FA5CAtyxuM|9(^_^K+9vn=wBN)z=+Qrtdc03ome2wdNw)cnd-NL>@4m^1Q(^eV~|@kDu2lv=ymWqk@XJx?T%X2^IVDKASV zKr>`rg;x|gQlShJNl+>VtXPgT8|!KpWutsqHkvM$N&MOXqv89NF~6?4eK79VfB@P^ zGU~1Vb&l|nYaHvYhwH;GmEALxXr-$FfCoM!E4V$eZ)P5%U*7Uc%nN7keeUIrCpc$gUU~WMt1)NpefHFeZO@l{w>0v^eJ?zB_v!nd z+j4?)dR@uAuWWhl?eCU`0|Gn-l%vf}09KJhqX3mkjjIYcXw4ZwWoCIVSO-K_nMF96 ze?bh^w2S4u3|tIiG6=afub?A$Kz><_=UVfO#v-{u-eH#J4$F-Nm*w4uiTug@zK$#l zHd%BaV>=$KS`>Shf;_Xt24zuo}{WAaa1Q!&j#tv zQ+aEKNM~(QE$O5pr&5x~$oixd>$jCjm5%D6@k(;=9H~vIT_3ETKfS6|xx@`*>bZ$AMDu>EYw^Ciz;J+b|j(qGU;So&z&)w3^@KKlG~rEflZ z`jxBCm6SaCXvv8)ZI89R{OAi{xBX1%_7|Qz@rxH;K3)3g_L8fQmY#hD9NFfv0YW$^ z)HI~^Q~?FH7DsBU0!6`!9k+M)Nt!L4sjXnWSco3B<>H-YnGI_#FX-qT>+H=P;B^&t zb>!y@(8+GBvk1dF&7A`o7PPtDP?#|${ua$1Yij3Br!s?SmbD+i8JiThmkgc)j8X$a zst(hioX;46v+e~v@+{aydRhc+^ZI~)p1TP?Nt6QcLR1AfapfBtKmdku=0fSkx&oub zC^t&Fq}H?|p**dt9IS;G+hu0OVM|(3h7C28=XcAy#ravbw8{Lg_MGgrT%%di)Gh69 z?#Rd&6OykA4Ru*#e7mHzL*9aV5kRwi_X56U-YpfJyMY8fCa|iKZ3Td4pqCBcTcD-O z&<)V$df9L&ssP)d#|FTfF;rZL$y+2~mGWdQd8n+VNg!*f6XdqEfVN^@OWwgcR3??m zuqI&o$rbq$kkFFdB#`7|@>Vg36=zuF3QXD}sY4I9)(Nt*u_nPXD}Zx^91ZT>KqoUM z&{YLUj(N2LLIBzb-| z&7}Zw`%Ob(jYLhQl^m_q*AnUxQfVXAl|$rEajm1CbB-kOu!s>;gu_%-2@>?RgZf&P zsd5CQs~ox*JZz-aN~(v0bvMxA8V|r@JAz*KX9EPi+m(#n!HrRJOp(s%%3w9(xaOy3K5`o&OJ0d((+E(Av4 z5U>G`hoB>n=Td+gKMD}RMc#6TbK_%lh}{GT@C6xtX;(~qOhatUe$yz`#fOKRV(KIG z>m1*P4{^_tWsi`zH1@A2k`trf&_zy#S+$$An`j%r5ey^H;r(0yr~>F#K-*Q9^HXLl zd?M!kw|gDnbui;`DL^Qy02`$MvJi9y&!+%!C&`4}Wb_x#p>d9N5OKtQdM&IcVZUS4 zK5p7~?>OCEcTiK?);|e|Ktw=@lu*4~L{X~J)dZvoQiOm41`q?68cYZfP)R6?Kq6d_ zB2_8UL9qftkfumC0tp~IMY`}*0@Cz5+!@9)KJWMbcr$O_I+K~5$yvWyd+oi}NhWKr zUn^=4`SfGiGx2Vjya>{$`Ukq@v%;*;>}iu$4l;m*WTd}23@hdveS|Ps;;ajtO-XXw z$?7;+o9J}v&=t%wG6q&IymXskHz_XM2BQjU<-Ppl;?i_UMcY90AW4Iw?Ugvc6m6g0 ziOFc?(lW)saD^lEVY8NIIsaLBMiU=30`YoIwYg;kL)?E}Gam^2Kq1mDJixuOY$<-K zh9sLs21zL7itW|u-j$Fel(7Ob63hsGxffY(haB!!XE-YFFOFZnKWP6Se zv=(*0Ft;sk73DWH79c7kbFDk47<;`k-;KAG*@Z{F@*dJ16`zdlVDgXV7`c*1B!l0d zqh$u@(50EE3qct61u zlqsPJT->#jvHe5uD@gg_>obg&dn$Vvo)?$J--KnhEv~#VUyDU@MCt{t?h!_luiSw+ zS$VqaZ#m|mUbE0}Y$EF%`B?7Rk-Rdyq64$ZwEi9uSfTVNtFKuv&|$IiVDLK7PJone zH9*4s&GGv_vPlU`D?1vpWIm>hIw@>}*{LV4e` z>uAiSr_&^`>kENrha!cV)_4VKRVk=YCFI*`Npm!WUNoLARCc*yIV%Em8#3nATs)Zh$X3`VDO6Acw zUMaQ$@bYn5$Yl5ODZ`=qf0|RO4m^2FX#I4JKkgl>qrN8##vKLaeE$eoijb`}FuvN? zxp%C&OO91f4(Tq#W1_)d49CyAEl_*fdNf;NjV;3uRfXv4_!Z)l0^2VtU~31G@WU3~ z92&@*MZu-PYkX~KF=Co_KG$EUN&{o=i@RGNe^}L4O6AZW;=hzb28tiKM`pedm~^(y z@0s6)EX?AQICiDm=#O3EPwolwxpY-4c%fC5prwc;M3NxAbyia$;gZUJLUp zi_)U#Q}<@>3MScv-I9&NGm6IX*ZJCw&m5L}OAZS>oJV;zlNj;QiC&ljb%9c1fpyq3 zdTmvMk`~&h{sGkLbi;Z{P;wjYd2p}dUs(}W&j!04@0&EOluGF zU>&lbGV@MyR7DO?a*DFW;q3y*w@c2oyUmOEMvmFYVnp7Nc#?E7LQbWQJcw&rlJ4&G z6VSqOjwwHWu2mA9eel7j;*@mq_Z7(Nr-;^*!VVb%w2*$w=MUkxEt_UEL~s_? zBKC5J;!oZ;6~VyEm0~$}2&No30&#&?s^w(EwB{`#?k^#sf9ATiJ+$;GRwxIL!52pt zMKj(FR%_==*+SU=VC}wGOnp6f>y^xdBh@Dz1v4cc)Wns*D#i*GcY$a3Ll>vl0(pZS zAR#8?Yu02zX9O8dW^%1%ty?yw_?BHAZ|Ns&2cSN^E%@A$Y?grW9=gu3(3NT}+D^IF zV*aBzix}$L7F+)qToLFSZ7b;;W&<&?w0kg$K98%?!H}%=KzkfA`Na&vF~y z4gT4wXO_V42vNjx(n4Q}&=u%ilRJ*As28UyeCeX~N-4{)#qFo(28d^7pS~)^o~22# z+m2?pdXN?dR4Eg(k^}se;YOn*;Yk5eq zH_AaiEv9%qj#=e$o`I@!Y0S3qcFV=^^Hk~|qO=!|78iSs7JGSvmsd%{C6=9yY@4H* zvF%GnZ+VO4v$Gyh#%=UmJKBdg^QnZ5xNtk1U37TsU^`oZOqPrNEOk5mN0 zRUV%Et-jZ_GE$+*EFO)rr{YTHTmKis|^Q#V_z+aU~+yIa|?< za~i?c>DSklL~PN^!52Ueh%=-3)Vh0Mi9?0`YRsW?=E+7ucKxKGoGy*L{-t*9Cb1nZ zBr)JRMVO1d^7;A7#nIF#rqf*iDrMwpvzy6?`hYqoPoJ-mQ!m<|qT;t_ zi!K6-e~icdvc>l=Km1R02mf!w{2y?DS;H6pgF8)n3;g_?z+Ur!siDQua($QZf6cbd z4c_I84RGC{+NRnbznZQh@Oa7oC|HDjnBye<*rA2H=n_A>>HWJFBZ)ALdmZwt^WNL? zn8z7LuYs=lj`nmhn<43%KsNM*YJ=JCf1dZ)lEK{;Y@=hD-sLwwWG~Hodqf?Y%BvPY zzc)xJw>N5o&;72z@sedED%X0`&fFCflv62xa)B-)TgPxo4|sCxfPQo@sjs?~oBEoE z9qD5=xvG7=PUQG+dwea|8vijWc{2)egR94(e-0MW*lK7^=o@yJdioe_P95e=P6#bW zOM2s8(wtJF%QfM%4qE&M&z|9rJna)N;6%Z4F*oS2?k9)t$9<{pyCKs>IBRLi@yTQ6 zO;A~OogYzsKK`I;efECLnCi!bmL9Ja4fg=ZPBKIyBjIa$f%{5Vx5!-g(PAOQ{ z!E8Ncm7^H5hu5m0L%${_Hz61H2A(w9d|_ zRfVQ2VtDb1Q>mOJ%1)MT*Hl6A`{X?xPupIf(HvJdJLrE}XJ!ahg*51YU{rr+mCW|W zah0S(TF*I_%7>`L^$M=(u1O)M-X2vS7O$;v>|SXKORiLw>@xUrL4$ygk_z<ZYp0Equql5%Eo?68Yz@Q!(A?lKTCh9bZasFoOgWXAO5k0f5c9$qe!{=5{!^myR3S zqV}6w7%BU@`gyuz34d|8mn9zeYE%)3TGEc{d37R8!?5Yu&2FK*tyXUn)Mc(lUJNI! zw#i#K9h12x(a;rF*+N<-*qN5nm66x$Xk z>#GEFYu!(})+x%65s5VQd{E@4D0#e}@1-Gsu@>Wd?x?1H!;22TL}GQj;R+;f>cu)P66%o?sM{{INf3eS6mi zbIVt;28RYXKX*?LBHlH?6X!=zQx3r4e9pMymH+=Jmw$rd?t^m;!2UZZkAH&Vi}fS^ zoh{;Drvr{Y{shGz=TG$CfcoW}B)-E@1Xtik&-mkU9(XK)@OKtTe}{zq36iI;s|S{# ztnV4%>*`PVrCHxk@8(ay2m!7E#J@G_dmPqJa5fg+-_;N6^Gkcache3qA^hkjS2s7T z4;I`{xUb8-|zg{D{E)p^UOVSjpqrH zk`RQ4!GZw-0)iJ7`XvJd1a1Qa1nL6?0r+P5lA;9&WC>OH7q6UC>ggJ|8mj77pi2f- zJ5ga`7zzp<9msl}lbV;MwYS@)w2PV+jxLExRWNd6q@#ecr+XS2nl@4D?WYfqoEh8w z@lkr$`*B=!@fj~9KB%neugJ&-RmKG?J~s=kfw_4#OpmKjNY~pyen~)Z2y{xzopSWR z*eAsv#E>lOECxn7A4NqrGHAFgd{Bgbp9F#f^A3SN{}>LKIH;Mo;H0Bb=aa}MB{W=8n=b$|GO>w? zg>#OB{V5gP`OK0LO$e`{UPK9B^aA*r3t>fEwCNl~2Coh**&7yXvrkabL%L`KBLN5h za`Og37Zm~^7#BS|D_2+UzQFaki*}NTWe;PdJ+6Q}3uoH>SoOy1OX6qSqUh@=HohMQ zwhAQ_w2BCcKpHA3GCav$*><4?;XB-cMf<-S^1%GZ8NJ1Dcuq!Z432D0I4JdKWOBDx zzI%T6)JK^&7DO?S9jP043woLS$zBo0>OWGEw22J zOP`Vj{QGumo_5RSh#I!&MjUfnrTRCzQT%y?ZO~Q%l@`*&nh+&SYOmSCn%Vs{2w0AW zhPL(0^r4dKFucDPCiIgM2n7;uf=j? z`LrEAIff!NwyZ}Mx6T*9pSw4`*8 z^u+r6$)a8uzudhUcZTL}TX^XKzl%$=i1f`M7IsibuOg;S3b*_7z41)`*>@9amzi9ucsaPo1Q1i2m^{f`HQWDQDRNiM6NOqnY?8apEDg;OxWh_yne) z-_hLFq>6gtB4}vXNi?Q1BJqn9l(;4SAS#g}aYMt#<&R40E&I!|0{&?Vq$@uQ!R+kp zw$X4w|AF2UL~NJHPm0`VD%-y$TlZ)14TH7D?#@LYZ}d^Ngiwgfh|b%83itCsOdR~w zzK9TiT3T$U++3c2o~HJEW3h9t7lgVznnRR0^e6I#m~+XC2QRARmvBhZv)b<+Nn@n- z7?=SkBeS(XHGo8s1%VJ>K?DNF0qguDuie5uN5#6C6k;o{{nJYvBJwT2^0;ojZ>Bhc zx-@k_S;RzVl|t7d8L0rl<p_=6d1cj-Rac{hVR6zwI5HDyipue-I4qOia0kemrU?*!1`+9?N(k z$!pWtzQxN^@?}7B4p-aM@S?>n{iYi)%~V~+=n`-PDVqOeP}mXDbL2TcrM95~P2o*N(B{+_t0PR1YT|K!ebY!JiI=%GI3qN_leUluKvpXh>)V} z60!DIxnBPefGSrreWO}cxboT*;$bHFz+o}tyg7T-<6S|mL6LMpbxfn!S+dntzq651HeALz zP5>tCyw^a1WoD@^2$$f5KW<6>&xWEEt)-KM%FEu9Q+xg1wE)2gJ5}>V?RLANQwLGB zI!S;&k-Yyg7#oT|Lh7D(4 zO#e^K%8NurN%yNhxU0nCxuO_ptK2`{=NRy!pr8rewj`2CTKT)f-~=C!@FgMVFrcu z)ouf4N11%f>S`GN(t0H>tvBxeaEm|F&<>2B(v+Z#AWuX*2fQdU{nDpukLSU^g}E`5 zdFu5o@*Ajc}8SmDDn!X7=838R_`jXEwp-b{ELOp4L?NXU8xs@L)yEO^RitG4 z*J)&p%JMan4ac2f8d>?+CF7@2pU(t>?b87w@#$xV)ST5ZkCk2Evc273=o+eqLPNh< zkPpHY#31v{DT@J1imP*s^_D8=u}g}k4H%d@9?4w{ZV)G*0*s}8#2 zz?sfA)C+clJajFJ)It$X@FNYHR95Z1toV0`=~3|ig0A#`ss^RBO#_-HXGZ`$yRbS~ zbW`CA4Dy~zzwxF{qleWOt7#(b+0KqlQMInInUWHy7@~Hvq*z9Y&tEtGBLToM-(dq; zZW5fzcU^Ne5$FJ_CD505OyJU90*tj51{_;ND!VUe`U)y4SdwL^e4imB82mGY0S*E_ zJY0{DQ}u$gsMv~8Xz6cQ!uT#<(d1jxu3_kN-u4f1bV z_V@VP{qZdQN3YfqIy<812u%2DE39vds_(?k&Sc5Q#wJP{;mosru@XsAipf_E^S`l% z|DS+ApMASxTWa{EqFe{V5|`DT<$vUyFH6yHb-C}3F};eMo~8`W^_$9)t`D=fzdye>Qy^)7lTz>T_WU&61CPz@Hq-9@^0J4532?;S zIzJxOQ%yB^T(cE4ErwUrWS8hHCj>C&SCQA%l;G6^XnenUpPrO&WR4B^ee)L96|6DL z!(s$*_zm(O9HFxK`g(iilq2o#_uc#+k?tFx!%^9DXMC-TwkSKxrbXsvKGTfe4P4tBq4_l~2u% z7asfq0wk6_v4CVo;(KtmW2r6X%CYQaOJb&$J|S&DC^)sb_>~>JyR8+fJ$UxaPtl-Z zyxJm*ubsLL3o7g_bI5NLCOpHeZl7S3`kz(>u;{cpo3fFc^wO_`Apssm$28}n&0Kxs z3MHQmU-;c4G5aZqt~rP~G~SjA9etiKU_K}2cv00I>&KwAW)j$E6)^<-A3O_%1ehwk z#wZB`-7Vwwt?%(6l75dSLQJ7F=piprTuQO;>@x(Ki1dM9OaANei^ zU`fc}|5>6ab_O?7049{13dLS`YG_K5n7jr5{n8DBkD1rUWiaWp%a`zjr2(eQVuRy5 z>+s;cWkLXlp`=AqKv$-pKh39|#G(ABFv28&7x{4G4O06{@r%8Q>RbLmSouQL9d||!}bydg@|#o%I_gY{)D_DRZ417>6OLFSt5s*c;@jsb9f#~ z_RoA+2c32@J>?au!SglJ`tG0I5m;7=uf{(}ene;kh5 zu>4aB;A=uT?*G}LUVO_xG5hU)#Y-}Bc$9zE4X7`V%hu%un7XTx+3=#zUsg!slZF)Y zjWbbkA^8g3ZcpbPj>A$KdV^Nexn49z1~*H-vy=KsJ?&T}Cc#0F$l2j^w7`Ch8FQ&W`h`5P3Ri9^}p2XIUUv z2+4WrA9to^!>i?oiJ!U}2K#G3!Ld<8iXKQ}md?%_xG|qW3W)-V))!XMQPaD4e@9_q zT9K`y{MT04XZ0Z`@|W8}AxZ96j9<2~cih~snTJw;b=*bro2;`X5I=51g(-bM9oP`rM1iK>JU#+rby=5vsR8pG-OYH9EOmFe&5hn`IO~J~sxgPhkb$pS4*AXo~SC z3?~ng2(JR#1WXXVgt^~?$GO4=c8YQf3RpI6nn*#sz2KXWo1?x|?PsdxseK~&_NC`9 zB!z^9r9Yko{M??j{7uN%8UdJv&#i^vVhbQQjMX#>wZ+JWxQ|f4=TiM&>Jv z1#eTZzlQJBlpt;4k8kgxmN9NM!YEkjwQ44f=fcxnaa{C&4Sk_*!li;1CSz_t%Yr2 zcucoV{sdfI{4I+;OiNgBlT@}Qh~2)Le!9!Wcx&KRh=&F7}u zZ3{x}m(>>OAA0~@vaD+#%~=3pVCO1KW0VBOf}bXz+WG6tq8F>*c31CO+DnBZ+TmV9 zEthuWOO(1YwfVEeWd-S`XMi+RkRM&!0!;yBSz80}(hW4d4*|IwPu*oksJIveNg`|W zQT^%)5)_yjUW8zB;qY5ELqOrq9~VshhCqNA-hchIgJ11dMv-4rgqM5e?KnTB!`F8= z`(|H)rn+7M=&;2T2dQ=pFgCiG5i1$l_V#Q$n2=AIOGd2I>8~R<)|*4TGQD_4Ue#>V zPz(9YUXW>R!NiT#ox$7gky8C0of~v0 zNFBxgr0Xr`VwDNeZE6)te48tLh(zmEB4G-9ej{I0xI4HEe*`$>opKgC?%(ZJGM`kT%73XT!?;_Q%QSR3GsQDVu!|QX>;$7yC{uq z5wv$_g@-1*zF6!?DCqE-Js$1~%OTu?U&U(C9xUNriz{6*1O~hZc&)8b2Cx@BGMl`SwPrr z6BbB$-t(J#!} z6pa}!G|dO(&jQhfw|^aul)pS4OETIUX-uIIOLFl0wKq$$zF<71Q7qmyLEMM05VD<%%z=_<|+>v7`KA|9#rVd@q*nA%2 z58>;ZqnQHgg$hlcjj^$^k!TW8g)+4!8)fT-gy`tv_h(%n6qY;NbLy_Ko|7#D!VxS? ztYA;pcRakP3-{j!CUlh7FOEC3nx3Q07Cd&-+qxMGB9mhwf{b~1l*nOfYv+E7 z2LYpofYRi!dz1G0u2Zh_C4JcInRa)c$8WFK+Tj%zR(7>2n;RKeA+CDHZl~eq!Ete@ z#;rdOcixyz{7Um-4j&*?ZNd*E8vd1aAV4FJW7ah^iI)CrkCTkuzVgMs!xt6eZ!STR z4?y25yajzwuw9OFlm$I>>l79C)YcCq0BIEEA{3K z>us(F-!Uh4u0(EaD#`HhS0?%J1a44vRU{fzU>_alwSbagArWw>>57t~H*;Q})eB|M ze_04PIE;WN+B4F*CU))rjF86F+VBa;{Puim!qo`QxOj!W-V4fTriq_=#w1`1y)|7` zfYenE0;+!cGXWb>HQaqjS~MxI7^X}`!FO3JjJ$td72$$MWSX##}m`t_UZsM)*QW0 zJ-Tv%bXY%=c9$L3pGRb%fph;owI)_Eh*@J5Ton!sppXP$+BNsBCP`qlZcMSll};F} z6>P6?c37DhnXQomq)R9Kw1NOf={+OGpR#P<-UAF$UmIv`Ub-2a@Y8P&egTTV9oEC- zLw;2<+e$ha(9IiyNfwZEF8F}mjG7@CQh(+wIiI)tu!J@@V-J{Lq=kHQ6eg2pZRDJ6 zXxFCMhZnL92)lx+SVTE8or_XAo%9sNoFc>u$_<3t(jy@3tVurTfdN1jNLH@M5LnFd zbonLzkBSWqh}@lP6KoA6BX{zP&h^rE>2ctHJymh!ZrfC8DNE9>sH_KzgT8f)FseL_ zK>ds|v?U{DkGO%yOVo>6jDvfJX*lh>Uwjd{S{oxW&TUfX{12~k375+~1+jGZNRssY zG_0T4uEdjL!#f~(<)Ab94G_R-2!S82;L_1Cd9f8F&cmYFjN?qo^~%!RG2f_6RDKQ^ zQo}s17No%=pW%xAScP_>znK zV>-(Ir6V~tv+M4Z;Sp8bTebvia#G?e#wwiCRWGE*Gs#ffhrYFXY(irr>sp)ZL-+>Z zWB2mXDrQ>4)5<71&I?<8*Dcn;K9R?ruCQ9Zb9x-O+1?()UIW!Enfc4S+N<2A4Hd~XnO@kS*odoY(b=*)5w5fZDx7i(@fbFEbJ>%4y%fO=lX=XWS zbmkTWM}>?(#e=VDNsJ60)~T%o#Zk2d&d0wU`%w|(4!_uv$H7=p9#xQR|EB<7EZrJB z9pcqQvY^n0s<0?^_3GePPsBlm?dr;w^`n6HfVWQ1qClJ0USV?P@F=FdniWPUrXvL2 zbKbBVDR>|8gGY=mJ-ca#FP^6#@W*}NlDc>ZmFCsC6sOY_1@f~Ulea~hNjKjVSH!N7 z>EhB)QHFSy7 z)#W(U2hPJ{wT7mDn&IDE>{c-9;df2-x?VJ`_|w{AT%S0pv|JPFyo8}X0IzT?N!xGV zx78>`bd)ewT{D0XaD|C7KoB@AZz8JucV>%#sSeFkxze&^!S~0Fh%Ja0%smx|8mP+i zw%v}XkD5qNm??)1k=ecqQk_C#4=e!-ngCajRcs|TxG`1kwK?dK`%ZHn46#Yq2)MJ> z7NN{C$_-a>Ti76*y&uqAL!a_&VHMXM*EyJ_d-= zxo;?5G3zaTR5#KL`&Lv~hT(EJh;g_*UKZBYGR{>mOOSq*8@y9O4REkmYJA0?d{zDf zy`@oUvDQ&}QfZo{pItsICM+bjK%QoV7KA8Ft|wVD4IG-`S5o)-9G1HQl+ zSrVK-xE_ORMl)Od~nnP5o9QI|Kri&?9NTi{9tu+wj1f( z`7{V~Sj~t@OJV{r3ymjO2N+EAeJP~v$zZ~L%)>HyF`URt>OxJ8ujq@bR~0=tp9Q;G z$2F#pY~o~@p?YC{0W>T#f=};ZTjhbV+Ex9;i@b&>?IE;Dw;ik9BjuqSgKs*uxYv+g zPo*>0)>4QR>kWC`;2Z5k$B(?BzP`QXu@sgTXn(%n%9bHL+WJy*Qx_O0zdHh>Kp}mQ zQ$Pa@m?yIiekxV(Fp6cc;0H9>`JUhtU8{%ZW5P`_)8C{$Vo#N6fG*FCx)cYZ=lU#6 z;G_F5kwJjtswQ^_4?$`tm}1byDMdcnzQ!Jz_bmWTCee9rL$ltnodo(~TcT|3+;L}W z>%quED)q<}6%=6EmBTXc_Q>2m+27?SJO+lWlxq^$SK}5+i_ldj;HoW2S&17duXS7_ zcRLR+EktgdR8$%(dYp;LPG*-g!yVUQTfF3g@GIkntEEEG)cwj3HM2ilET-V(#HC2L z-9DArHVyCf(0?JpXj?luh~PlfS`LV+tnR=p#7OGQ{!yoW**`%aulqiJ(FT8zr-M^v zM+f)pJm-|!4w=wKUsJGUU5GJ|B>>8Qc~3~oht{3k^mTJzk^YSOaGoyES^_ImHV2}% zsNaW@K0#QbZ|OqQ+#%$CMHn9biBk>cd5TSVMn}#S#jHxP~z@!%sD3;bcRTZ0C?5rFv zM@qX*?Jyh@!~ga%|W4DjlxCo$l4yi^w!NX)oG+6C}g5CS!m9JbsvluBxOB<)e8{*#+)DUJ*X6kh{-mCj*1Q}U>*FoyWpI`I(Ho0i-p>)5z%Iqt6L`@4 zQhewVh(zE%xqvC(z;?IAwd}aN1);vWc@mth$!22e;(Tt?MNb*Y}3K3zhgUmIGnfj#~GpOEn1i+?-l> z$8&-ZhY;uby4f_a;yDpxek`c<+kvwClIj& zu%R&8@VhKt*ttRK`M9A761H}%2I4!69h`kvo+>rnyYk-p$vw6cUK&fy$KE{bA`QFZ zAgagy1U{G?TxU~Nexo813iJb)=iGbv&9(1B6rYh|9Pqe#2`EW3LiPNxmv@_Kh-u`E z@eiv#?N}$XX+<8V%P?53V(ZK_QT4*=BNQPnG5P&fT5c520OamaR95Rb#oMG^0=#mQ z*t&?$DbNH5Wi(hL4Wd(Sil+Tu(XK6BOD$_225Z$G^W_zv>ci%dH>^ds5ow4yRTl3# zV)NcT^os*X6os~Dc~zVU{ytWO?7FfvXN>uMWf3Ss?cM8nqz??U%nGXKpOR6|DN7>S z9c#3lC?9KRw9-;f`{CT&IIHWqog3LNbLVNNT6u@*f#xRA<4kcwv^-Wf;rQS0hA-Zm z_4Wu=OKmLp)4OGz;$i}6XMBh9|E3Dp#{hJKRb;-42 zi?rLRJhiOr?Y{?W%r+Tl71JJHheGfgK&(2v!C8XZF!4%tJK1~>F%|Bs{B?zP zqDDDJKa@Ah9g{mDpaGKFj2zqcOGF7N)g2Y|rsa6#OOf1YUt_=R_cL3KA{aa4sJQt2 zrYfuB=L-A~9nQ9_jXyD&0v_sHhCsq;*CLH?u3t6Z#I}P)fh1C$5kGEOP(Hk58@w^U z_k?gBSR=r^V#~_Z7*4FWdVaiH3kwSi;2na)qL-?+IGphf(Fre$NRmNo9BD^S#^G^0 zINeBvG=KE-&a3<3NMCR69`Dgvx@Z=f&Hy(vPkQsyZr_aRtM`x_ZJ8y+(ExTYa*dO# zoNxB2Ki*$1oB{nWtfPYYQe|5u)DP_`#$doUn!$MLlUb)1@D!leD)Oz0N2eDGACP7H z|F#JkXH5W!9xU4+{V?82=cY3#a%~?IZgsK<@Gi(auJt+-|=f7BmD85 zvSM7v;EjHqu^eQ+SowwW^$Vy+Q!=3>bTIh!yQu9{pkHtj4Mqq$?5MqEvL+k4e~uy8 z3v(@`{dKIo0nBKsGg^193S8Gtq%CeRJHqzjETh(I?mSZIx73=j!Aamy0{G{plH zW?a9=h)lcll+{i=VyLYN6f^Z{r^Fp02K$P4xRJwqgp`$g0!*7L8j>yaF#o*L-Vy}A zB*Tj!rM4NB+lYLC=9>3it!DfK80jv16`;$o&hre_ z!1F4iBLx@0+O$%q3vY!v3*vHX38uQA;p0i`A1lJic3%cpo)`n?U znh|1sg%o?zfV%-So%;IYJ$jiv~B3 z4ICN70AHvP9`RHwjywD$${W7M_=14*Qmye!>fT5~1*_BLCdRacg_c&Pyt28dswhdj z!&q^K=38Yc@nEEjHt_&PK5a4x)31TI!QrAycA|`54hEdPF&kL3Ntt;9Hl=JRP4V%} zU0--QTa|T3$n?=Xm4(730~U8RUq%kDdTDQ6tuA13+Bd<2z-#5=5@Q?2nMs~iNsSwQ z#>E07+OM9{W$jF0gjCWT&XvCwzp4$^i<?4u%GGSqeEnNgrv|Bx$lFq_=rX zBpfYh`x4v(CTzOMn1I-{5hQ%`*clV5%V1k;O&bM3K(J2F_FY?6JFw_$ZD)}9!sc*7 zDs6lp00GU};h=P}S~po!bk$YNK_EVD{9AdBI95-Lvu1LjSV~1h4P&c>vE83Qcyjhw zz}AUp0&|ylx3VY&?LR9H@x*f_!NQl9q7)^eLQK+$v&D`rR8!&^@!yBOj@_!*56q1x z@4q7dwTy6 zgf^hL05M#>_tA72+KEy?UjR(#$u9n1^?Tf3otxsfu69j{71tACWtR^)`?U2F5HAH@ zRPt8}2pJsVm5#1SPH3`0ium69V~OD5!*($l49rsK^@qzx5@;XR0W)+vM8t@F{i?FA zH=GDJr-Mc|b@r%{5kCoH1OBg~2$dZg0ShbVyUG${`OvddWa5Uod{UPYoHOEOD}tD+ ziLMG$@?kU~jU&TPX9?O0+!+S~Pjifev_5`}R}rYWiGEdElO+d3L>_+IonGnn&Kjc$ z84zhFUSm8HU7F7WMn(1|#gUYo3-)@M6*N66qQ8+6V63w--~>22C*>izrUHK^&JGap zOXAvjz9W!?ih3`)rK65i;K0tfC_yG(*W9;tyih7R4bo03S=s<52Q%(@H zt|t^5@;}Ib|w*xi{XuHJ*aqHSfIU5H|+a2vO^PswqU#>l_g|J0s}N^ z&EEFh0d0_n-wfOYKpOr~Sq1PY7+4Te6Vc!v>#xWI#ASd&a{7G)sK#$EM6}6CY#!Yvxoe*kRG2gwss=ONd7k@YAII|a2xhdiWce8tpfbir- zx4x>NVXmS-_T<}^V#-=oO0Vy4FBxQOC5jbTF@EPwwg(WA22diV*f|g^t1bGDIRahK zl6QWs8PLk?2>0h4h-nLO{?e59U63Chl@I+Xr=;L^)YzTtx+}l*g}NeG5;oH%#{fRD=40^wT!?Jev3Jwl9LOd&z744VO|v>;iiBH z8-q+1$)d2!PdpnFFW*p(F2_B*K~TTc`gL$%g}lFG@_yVyRNnVjd~-1=&TXax#W}(3 zsdC(?4w$veGQ`dy+R>*Mz31n~#fGK@X`HNK7sS!zfMjC5zh6My4^vsv(;R5Gh7*dose_d|aJe;M396iX;#KWYREEkyLT#0;mmH`vnMduHFh{0+Z=SJ#q_S?p=Eug{ZP`2BV+gjI9 zQA+I?;T|k$eJH7pC*}*5iT>;8-%`XtEE)F{lPu|zmY=l@shr``5sxstWg-fw#tWu{ zB-?Gva#!Sf4UwIXQY~wdLg57tvYUY!OY-0hxjad06DcOSGi}KDN%F@@wk{~xAvRv zp&F3;yx(|WkIKi2EMs2O;wD|C>EV?x%alrA&Jo*rr89H&f$B`0@k1a1pc(G z-fPmIDg;e!2QYAIA79j57W_-m#f%zl?!nbMa8nKADp2OMo&W@EtcrslbUzIeeL7)GCUd=LdG6`#R2I^l?%afNns!vpafF zd&$rbD6ziY<9Q%*DX;)T^!bpKxbkj>Vqa3w!JDv@!E@|0j+%riM?!z#dMH-si^k4s zugv3V@xg?mcVB&Q`GDz*SJpo=!SWD;@?}q7vdTs7Abm}MQLd-}tF)~KB|DKs5-aJ6 zbyn|^4m{t6oN7`LYMp5{f#L9G!3d`a42X z18;%Pim#Os+a%SAaDUudm*>6trD!IgnxdH;icWJRw$acGCh#Om*#E)Zv|-)s{9r98 ziSOBR*!W8wcucF%^!QtnV$IwBY;lil!Z`KX?2=rbxIv!_PnB|&j(?*zmB`o+{lnKQ z4*RUrXveGzivh*9*)C&K)`G3m^P?2A7yUoi7$Hq{5-zBt&dJi^e?L80A)*F6=SnvJQ}S zy<`OzZv=CXHBuQknA=c0?as}7t&;dSHIesw_;g6v5rTAj)(jDH?xyiN9W&Xj4B>3s zsXLGFwRmQP++V-G&A_)BH1hsLMhTcWgutb`X8S6t+@&uye9(IMh{Lu6es(Ym9K78b zji%mZxNknyPOtS?N;4^!LGoVJw-Zn1_Xq#-`_laxJClE&Z@1RS9zmf@4-E6D+2hR- z3Rd~X^*DC~EC(d&6Ps#06*aXlv*kKx$~QYU90|gV^=fSH$*az^z_S*OcGNXWu6>bt zAq}e0qZu5Fk2{u7{2xH6IA>YHH71=CV*W@K3sxWd0*lS|ERXMxaRde5gU{y;&(kco zu<9i7kLL>*bzaug*KM(z>q}wYj!k{tTlz2G&51W*jkId2J%ph?PQh)55U{dl72rr>WNq9 z@7>q)h;m9KpBr~5R0Qk2HZJVW?kr5_a&s|6{Blm4bziC4x4dC!c-|u- zNUSu!00L)bzS+DIiZf$Oh#y|j0Y|nTf7%8wCd+WB_iz)08w{))4~J^ccJTCmiZ+Y0 zP1@dm#X36pL;cGD;jTZ6lV6>8d_Im8Gc$O@OHJ>L|IrYgb3JA0@|5iBZEvGz0AE5m ze&!KK2FOTm=EUjs^2Mtjgr_gZIwoCaeRU&rLwC9Z@h)}5Gr#&?coFLgYx4_m=s72< zS#6@$z7*{FT18x)*l&8ZMqDV+W~GCr@X>_?pTW36aVR(A9|nV!x(VqobreSF>M06i z^p)mEp4;0`z_=#Z?X=E+&=7t5z8IMp{qZc!o1K-xkU_k?CdS8BR;h)CI3%t zG*fQV%gWO|i1^@W^i(s1oLSgIQ?(!=*9EMrleA9tri`ZEpgxestX`rPs8QU6?X$C8 z%m7+_1_Oj=0_t5eO+i<5w(Hb?Vv4~vlCNU>p5(BuhFr^sTE>Delk8qQAnEc?tO~Qn zz(OGIFK>(5oLcFqp(QjP0=2nHLIRabvo)m@aQx#wg8|F^MnKM3arYks7p=KMn#Pue zu^(L1Bar##PBC0+(PtZWcfLU-ft{mFYy*NG&mb!_{=>B&!LO;*h{MCfm;w_d*wJ2G zm2DH7i;cA$lK4MVa{JW4WlA(|vtS%0&yXAiq@|^WFHY$nycIL^cj8cd*$4Z#63i1rk1!|g5>+28>(FvFthXt}wsoH;? zK4uIlTZj-_tpvhocr1DxKsI&P7ZMioepn}AwPQrAALD|!NN4s5@;&C;S$@te z>-XDj)O)-nMMA)Lfl$;;@T?gEB!`{WgF;@?#1dW0`s0&DK|j8O;o?U?N+h1PNGa+}Wm^JJoh* z`|5rvEi-k>LIWmQ&wPTkt&ct3vChI@y7YU26S67XYah+eS2ymEruM(T2);bXuIaCz z_2xP!KZ(1#T$w#sX>iZAqh?mJhGZAM2|>FXPglto1Wx!OmmH-3HW3hFIYYZWS7d%L99xeK&ABr?QR4p*{nYGpc^)+shD>TU+V~ex#>{D)ulNzhWq;&aL zMq*fbZiBiaw1He)u$czMn{et|amPuxU8k8|ThtJ_k>>da{*;_*65OAAS1 zUh{8E7S~dmD1DNXXYGq9K>{GNDC1^TE|KjTh!cMb=l5MXw0*I$FFik07VnhR$+PRC z4Z*>)NVR)K%lYL-8k7=y(-+odBVZObaJ;p5DA5I;0DKvHDOfH@itUX=7%_pQG>^T< z$#4{d68tv1zD!S-Q>Nq$OhVZdlDlIL_vxZ#N-j=?vFH7y4i&|k>5&yEckp`4p)b;8 zA2x<8sbA~`CdfvGe2H5uLRx&lxZEII<|p))&#&ak1k~SWy4zm0AGJPOQTXt^$HxSp zXl^c7`Hj9E+fbT)v|4b2oMAszL@-WV?JkR2lZ9jt|L8dK0kWj4)BvnL*W`5XNEKeQ zBK+9!(Ow<&eUjj4*a>coQIJYP#j`v*bL8DqoiyyfOZ&>JI=feIc%?0!;OOBzlJj?Z zK-A3v^cw+wkikRFF)qWM@f*qBE46g|Nl4_KZ4Kj4>foE5z30K{HyVD~#{Jc{t(zBS zc9J4*A=qgp!+D8hCby$0d$%daz1U1a0}e?Z{Azo(n>2k?*FJt*Ov7;vET#MvEet4x zHQO_3$&g?QBlP@a4VR!}q1XYBUBSfBR0g}vEKOlG7%T@|Tf?VU;S&|6YR|kZFw8F- ztyHubiyV&|VXg;nLMX+s(fqUh@YtTYeFfNe#T3%ydw2|dg|o5!VFvoGL!+MQh4gJh z--2an;+}rr+**17Y9ae_w1*H+>mBdVi^p6c^;%&*CEhLK&v&8>)>67J5e&3cTot&M?Gq`4%nDZOn8rvy6|F8)z8pf|RTG&U_h^hm@lxLftpWnb*F>xPi zOBb?qTp3e!7`17;*m|X=8WtOr`J%Kke{;sAw|dkh8+bM;uamo{K}vI&_EbN%VxZpk z))=B`lnC<}8xZV20So5KmA)^` z+ZRvSZc$%BHU_PqoX;-i;9D$oq#==>y*Kn5< zRR8@QmC!QPvP3}_eSQ&Nocd7PwtVw#vEgBS-Wuhb>@Hh;bu9JHczZu&2AE?c)r?N> z@{A+G7=C&8}%VK zR3fFx@G9+ORiTGU7Y6B{ThwnqD0ajF+oAGcxk>(A?zrLi=L#>YOQIP8X&v{@mn(?c zVNA1N!f+J61*N@1dew8_Q&n!n`T6Jtt_vy$f^a646N^&B*@txiAQlg1&tOcp2vSRB z10^rQLx=`CUQ+WyQM1qa-p4CTer;>N#Yz8obf5k~Pi7+@D_`vygzD!x@A)OwY-V1D z*swvBb?r zF5F{2aC&v~;b_l}nJ-6Bw@DJh*ERV=7lD@TR>v)%Fjl73VrBJSq-dd{{<-3JgI9jJ zX3IK1z21iVVH-sAD2(e3!9^8Svzc+D)BY%s$9&AhPvgbB(?xpMa64qhWcv~6iHD2u zbkq{#jHA`^InWylEN9d2HA^6x3uWV;cS-ilQHVSlA3NO#h8E(#9d8;_ZEoZ@iqgy~ z3?^vB^vkiaWY52W%M^+7ySfBmtJDqau^*0B>k73O0|5 zwO=cC9IE`VE_!xrkIc&{L)OeuL9;Zcj&l+i_&js_$mN8ft6{|k3JnpX;gt;vRztv_*p{flIHg`PZYwwP8_`j%W$_yJf+IeM^|nR=4X#=W`*Mf; z4jqiR8OhUn@w{X2wytsz4+5NetNBd!)Y?6L48I%;ZR+&}cS8it#%cr$ZcFVPRs*#z zSN7i=6lY#le>d0 zoqT?@hzy1qGg(Uj70-{Kwrls2Gs+a~nD=$&t@JZ@OW-VuKf5053KAUgjy#GDKb8)n zjbzHrl$=lR8|Tr*G;w2&qXHC6He41bJRH{<8j}}g!s3c#o80V;BO?DifTSx_W+;V&Jv@F z&T%^%x7j|mBm82zm+5#!cO^z~%{3^d&*$5IaGt4R=$X@QK1^I3)w`Xx?!#X6HyT;h zN~eb`Wl+^wC#2_dmA0uJ^s|s*FnP%Zb6n}|-MTys*0bWXH66uBbi$>ccLk{z;Sg;do$^eAmo^65uvHssxs38zq}bNy}{y{ zT|vl?v_>!fD8<0!9?t?vcu!;HfdgHi$;aiqf-{lx(B zY|F;969xVla*MML!N6z7V`Y}xD~bHO=(h`{vySJchhT5}i1=JO4Ai){HzB0g;YZW4 zaE}WOwc0Y>()5p#nLMQ>miy{S;ZQ@_>Mr%73u3+~7YIY^6b4w)rl=Tpj0u{|7@z4unb$X%6TQ6bQe^vl zpEWodJp-zIf#*AWs0^Vk(bw-JTdAN81@lJLS7$sYj?_;080N%Dy%Rz*E*I7lL3SJq z)M=(#Gpv^zXCEm{miLJO&eQ6haV6;YWeSawjb69o=V6U6+bI2!^Jx7b@-|_1>1@u{?%C`S&*B zsd0PM=xQf`)e%q^4?SjpYbvNPRTA1JK~zGNZUhF9M!Jz^Xz32= z?oOqoVd!q8yIVmzhwkp~hVP8O=Xsy^{o=Yl|KO6t%!z&W-gm8gEi4aXG2u7o0)gZ* z>zC;@p2kmVk;DBr9~%q76R)2!oX8DCn0VS18@O$hDAJxO-$&ELk#K%}6M=^F2X$1zi1-TDc>FS6#%~SCiD@An? zgbVZ{H+yx+lD(=Lb9j85TiM%ggWSAjlv8hezUlq8UVw_$PW|lvVK@RZG8k@DiA#TA zA=nn~@|SeJJ6E4RgFDWip0aA)B~1&G&kjW0b8l{s9l;$}s4F5wN|6yPV`fZOsf`a` zC6L_ynn=^`(HB<$l3S6}TsF?FQW%4dOAye`aaDmmA0nXH=K}*|;&j^vwpH&1-yNG4 zn12>;X>pu)9V-=5NN>Fz2T`+CAnJ&3nSSxj^FQ6N@$Q07SM&v@R3u=+wjfwP!QUTk z7!0Qn4h#70cZt%cUgf!jxE%` z(wc^zUIrUl9U~79!B%{pUs#xjNi(@B4_(_eXOg&?7{TDR8`u_lN=y9|S{>$g_WNeh z6v?f0y|qnijO@F$^yWz6*qXWQtJJC7rUiM$6#YaEFT;LDv;-wXPw%7QB0+=)kB7ps z9LMiyEM=c<6VRwPwugF*z}thT+v z7wybbr20Z@5lo{&`=7^ry#WGpn^^LKx^l=8x1RXOjb+rPbQDZmZmD+xRMT>_vSJXpiFjy+w_tEnkUjeGi?oPYVoVH%V}m~C%GtH+;deeSgO{dE>pj(TfdHQG97PbRVmEyVLCCv=ttPOM6W z?d;u)eC1y|7sR!S^g157z6{H^3D-pWpAc6YGt4}dfr91?XAfJjYo$^O`*J$FzaBYk zp`bg>r)xkgVe8>vBcZ6tL~f@5m!3SVS?f4~ZGDuq$VXCJRO*hBGTzlm*XC+IJ^N_( z;@hTz2joJjvM1ka=*FND0?%pL}A(aBWBo@b` z^P$J#d$j$YNHt%XbWulhJO6X$MA~l#yr%C-5Gn-am~=k}7|g?pN&S^ehTqlv*_{;2 z$Vw$;t>N`}#xCOAjdu?pE>}(W3!7KGIn%$WS}=Vas)(^2bXa}>d+Sy#`i#xDAMn(9Ep)lop%$qV1Xeq#pwrkzvBLser5)#0SKhl}zUL+qoP zdjcF;R~X+%@G1m5`sVg_>f>SK`$FXH^B6@-IKmn~;+K!Lum*CH7&zY-X`jXI6GAm* z^I%IPU)4Zjy>NH0tHriu{hm)oS1KT02B)^_u!fmeW1R7tlvl^dqx>>}uscsOAePol zOj+gUst@#?)Bec$;)M_8>iW`J$WJ%FQi6(UDe}D&w8WAIFGk6THkO!ruP=jN#?wLy zc~w})V7b@fhN~7GT4zVJz6kXrl)J3xC7bfX*i$X+t(JA-rC1~!w+X!KW&c9gbKKx9 zHl&2r`DJe_Oxrx?7q}-$QrwldVcKFp|0GZxO@#nbXnGsTXUTwB{}uPxMf0-EqT(4O zTAuZJ%pakZG8t~77VLR2Q!D_`KP`Qp*(aSU5yn`{RvP|KQ)%z-Y4NjHFEa)+KDPet z^Yo)eN@@fSPEig2^>_fwC{-O*8eW=H+!Gp+(@r=yQs=#er`ng1Djx`)-4zm zRAe+tZLzy-LTd}7hg`}^>WBpcI?*eN@iE; zv=ka)5qa`RY0de_I_ThtcwLbrLHSzu46;MR^7DK`&kX11rIVG1n0%FDZ?Saeu?$=3 zp7r(BS(|ruVoq}aGQLxa+Vfc{Iy3%t{y3(uyT^6J{Ct~Q{X`Kh`b=S;6NWfy%~qQg zk~d|`w0_q9ET!fqXPWMwr~KGQ-4yu|)9ul_+qkAe@j)Hp$yQl)HKi|T6#6Wthp#M& z&kWssb9wO1^D??x%LP`2nXFmo4WDOc$j?Lvy1QAmbi7SBb@kMZ7-EI;vqp`K{qWWB zl&-v1a{p=RJjc%;8`*TnWTj5&8wC-=_7(}jO*T7jnVH%ragTtZPVb0fsTDYU_?=MH z;Y0R^W8|dG#KcV6eQxvq(h>!3tEf;R$)^KiY3sIT{=jU-SL!OZ) z`|f#zr#G%`z9Lhx)dc-Dv{zcF5jn}8jaoYY@nE9{Asa!Po4h5oP_JYgJzfSSKX2$q zKaom{K1 zlPGWcYEe}iRrQLMul2Z-#@6y+nSF{-6s5r{<-7=I;$g+@!QlvV=q)y}$Z_$Guyq(w z*ulcxlfBhLo8vMVTo*QS^f@B<$!xBtba3q~;`7J$BYoJGPOaIZ?AXA1j>C=#G(-at zL}JflC9?#W!(nE{@!(Z>o6Gh{#i%nezxmk&f4Q55M`gIr*Y5#wR$2azVeNTza}B6? zpKev>+_0?eOMI-@TyAb6E=!u(t!FsLE>@~8f31w>3FcLf4CEPo7K{JJzB!cC+>q3| z>%5b@+ce@TuYdEPs&vV7mg>WKQ-e>YaI#9#MT!3Kw<+V{g!+lD&vZ;vR-?>L zI_bHltt=jQD@gjyzTT<@yEv59JCE^myA%D`Q&~F4EV%7q@4uss6okgGsAF=xY*UoH zWRibh%I{?;HQQ<0RL1Kz#i!J6fUn3JVKUS6sC1}i?tuH4pSS|mz51B>64wf9JspN; zrl`3XOZ_?O?n|>i6^(C_rH6IvrPby3?v54Zunytz{Y7Bcuaza~vHBO}jwRGVtY+LU zj@y%Sj`PziuS$wAtnpp163J8QmK+NkZ+;&yP?m-tBZaW4OcmixHJU$ZJQZu+%6i%# zJX?6+rg)oPdCxz*tV+W>w)_6#V4(S#N3b@#V5|=U881{y1xKz;%h$~Ms8#u&uIwTKh0^q?%x5Izq?Eko`1DI5 zGLwb|GjGR=6Q#JDd_N;6 zz&>|j{b|Kr4n#T6AT_%3*ZF=?sL|cobWu(}J4l<*KhCMRqu2=R@QqBE57JabG~v@< zD%>~|WD@X2RyO5WUR%cMLd7J+0;ncLahbjza6bR}lr-i&Qj+b=sgveP%?0w4d-+T9 z?78-?vE0PbPuMnuI9?#RU`!Y2Q2N!p$H`Ln_r2`s2?mIfvGew#$yyo>QqLttp^>j6 z!d&R8!0pVdRkM8>EFc!0m?)04?J9;9*LSD>uJCm|u4!Z(4%}?OX7JmWnG!*RJ}gq} zarjA%j&@U@(@oMwr;MkP2{@iibSGU5RqO3`BpeZ4g2PaOuJk+<@Df3+2de4)LaXdU z?b}7_T$!@;dcSX^YRvEX*`OQ+*=_X}J90%^5OA}YJK2knI?&WA-Gl?Yp6V|c zB20`-hI->9tkCCtkHf`{+H`lj()$V-EpJdcq}0z@^Fup?Un|KS9i_lOO%ZIjxnyPu z3A~P_Ny;6_V!4>ekKA<&#lHx}d8IV1Se%s&i31n{S=~o739+q&k=Jqq<>q6q+J~V1}%eqkfllvGqfXZJVuLPQbWE)Tag zt?0SJpWF-?|Fe)oLYLdy zTRt{6wkfYgAw>?IW{>^5w{u5$M2TPHh}<#Z=YJ>fW7?2)Y8kDfU&dML7%g}$&m9bW z6W}dW+qsEV=z0B6d$`nQEeNU4dAdB9Gm=hb3q$i%{>Vk^M@d*}ya(2c%`?(Wi!(Df z+P(-Vix5Kq1J5kLr0qa*cfLEZhq&{!^z_L7#i7C+;-#cCQC*<$^APj^G}IogY-}h= zRt`*8=%a4$?ELY%KK>Iex4_&U3~b_ro{B?FHV^_ph4^KIMI^P~9& zdoy4x0Doe9Tq=pfcJ)d8DV9dX?dJM=S|ToyQSUbq9bHK$fbVCkcRot1GVlQU0IX)` z=G-5zPda?|n+!aFo&bKw9q6C1b8Fzg6!HBVkzRy^sn=~!j~es!PyR^Qw=d!06{BgH zT~F5+c6WEF7<5`KPPa$1*&rtXBj}XbWbqE+`}bciu20rFKx>4AgiDUwBP;YfO8~f* z;M?bR&;u}QN3+Eu_=InVfu>l-j~{#FD+d6k!MYHue5N#B_sM-&XEFKO-T97vf!oEd z7YPZ;=1R$|E$-AlJe&kFm`UFw~+3|O2dBIUKa(0 zv2qfjfE3V!SOgK_$-|v9S#LFf1h@Ra9)wE7V{&zSu_p>S0p2E;rIi)Y>eiOZ12k9C z6SSsLYjIPpR%J8{V|WC{mrsjn5ej88S#JMk41NNLiA2T3#P*`SD59wnn2m=hpx#%9 z3vZ7WTkK=*z_9+0n*Pf05(#;inWAde2f23x|f)Tb}+C-h4=ZY^OBFLI{M7k!C*mtuZ}EHTS(c1nf3& zA-Gj#n+A_h!3jr4Tc7K#y-Rs~Wml8XGH!4cweaw2ogZ7!yO!JeFkyq}3b-litRr9Q zfPjFV;2&=?`y@(*OSc7!y;nPfGC?y`xMC1N5Qzwy9ugL2^=2+M^N?(fJvQ!P7_IS zw7BzV_zd7Qld@={5JU0G2#}-r3q?1ruCA^q*x5y(Yu%x^_HvNOTQOmXYSEYMfHK(e zXm+dlZF;o;kSJI%81y-JN(7P)ijwRTbmIZ*g3;tduo<-Jz-|)-3R_jc=h1ATLB^Gq z5P%M*#;`xGBqo${k1|LTVi3EC0Pceb)9=9OTACrkhd@VowYvtEs6+&0YNAn7VWp*3 znqE_bBbh?7;%E&owb<8hDep+t^78j6n3$mTv}O2^p4f{<60m{Yf{Tj_3*{CsRUHg_ z4F?jPZ9w;=3VD~x+zN1N&~Xhreo6#P$;7ZQz0RoZ2fsc9@UjG@?Zo)_s>qeGUY2c6 zfSJFW=-YI;UI#lw5A7$& z3K;7L$o|iB7YGLAJlZeDVNkHw1>$1{qeV4H0A=?)feNTv=zCiserjHG6v}1VEvk)1 znFc%sj$lYEL~k@K_Ij!mFxg9#tb*igz^l5Kh3G)%KD^NYx1GDllc_)f`k(8ToPb<| zgz7+%N6KFD9VYUiufXm1fy+Y1d&r-n*USF-q%6c5%4$d6=yb44-O>i~>U|V13jz6) z-!br}jrYW!N9UwEglPizcg(`fj1-G3dE09nrH##LweAfd>J4^IazsmW(m{c_=Iat& zR|j)fSvelBMYP!=`e}EKB*2*;fcXKMq46BTa9oJ=vr^MyhdB1SFGLQPSueG!N!Er{ zjsk?YR9WU7vJPY9x7@$;(~^=3X{agLa4DE08Ek^zqLCNmLn|J@k{f3ojo@zJ zq6=bTfX5*@C7;jRJ~x^Ijho3GD+a-jMv?s5{$59MO<|f4-6)w#@yJ={oPJ=;CX@NS zA8+p`$qP2(@e8DB3ck>Ujgx}34o`9>^#tZ3$CU@9<(odS%i}{F8oHyCB3@bw$?q|- zgXsq6#Y<$M8$_}Yci1s^7Iv`Z*u3~xX$VG)C`9|l(l>?WrKN_vIU74fH5$a88)abb zwMWYC4g2&RJG;Dws=U4`9yoZjj$R(WA;VJK?9-O0nYDzOjRaZmSMUo>JTNU*H43-{ zIxkMA2$ohfjooM@;SUynVkFfwF~L!KUxptO5BO5YHy)?)AZfraQE{Ln&&kQb`I(z* z*mhq`TKg^VT_KJtt+Eg+YmLL`5|&35w_q&tO-gvvr5pC;7B=HI&ceI`XGroUoA*#o5K)RA1K7>gl+FSUwZl)5pFQ z8>X0;nAP6?E|Ils`K5|K-sU1#WBlTfx^m3k*dB7rh*H&xuMItjP%QILzG6Tg;@6Ky zU4yCo_ns>sYBIdHx3@ihF%Z2bQ8O_yVIh~*o1U4`l6RneoykNZGcCdUHYv5eL|k^? z<91(xA+;!nqUSZW4N+2mVRH!P<3SZ7dL_|!N8Xfy903+&}h9kNy`}=DJJjj)17YaawnUV)LCA?87 zWn+7w@Hz9+G#~s%jzU@51h@d^pLc?>ZyI)DtndLYUknmXr3@;J91M<@ogbKtWh#U& zwhnw2^`{N(H;7rKUE-lZlB^%i`0hyv9^RB{x9r9wCicb|F4}Jm)$A@bu~XQMriaHh z^C&h(OT}Eg!EQin$XSpvH{Y{f_jb(|0m34m=KcLcEhw#BU=z@QeI*Cupi;z*6fRvi z%zq7VuVMItqp_nm#0HyR63nn6G5Id&t%uUdrF6fF;gF(_ryPi`Ck>j*B4Q8&Iy@I! z?r1D64|o`OH0y0v@@veeh~kKh4Gk}NuRH+Sz$lt1?!P-KO_44TeHKRv)0^&TC@Ha` zz#WdVdh=Rb)~ChxJFeoU|`D~&3aP1{;uj*+3bFG$Y5x=0b_5H6A?_C z2d2>rtdLmh+w|_Ovl+om#%`YVqu1a~tQvaYJphi*zWaJlR1~S$?d5^{m!W!qhz#K9 zC5Mq>%ZbQ!bR8GfhHDG%IulxwJgNLqgCir;m4D_|OQ(CoPlu?P2C|zAetdEXj;D+& zDlJX>or@BS&z;0a*^$MtA;`+gO8jL@gU#J{B;yiC)FgX>2~YqCGgtUokX9KHh_MM7mG7zv3m`>=yHy7HxAx-rvGP2&2KI#T*4- zDFgh6Fj6GN_mK>351PBFHw-yLegmD9Gi$uzbb(yXb=dXueqr%?c3WoU?#i;$_U!MV z=vk)&3Qbn-#I0yz*>NQTml{xUrn(sC=b1}s{{AB6Kl}k)M1JM(Jj_R3APH}#*nFA! z=wP}eWd4y0L6HZD&L4qm69Csv{Cn*$p@fqrSD9a_uBaCdCHw)^69dblUf5{9WIsbVg|3K*#V;}yY@8#B;RPJaoz3FKaTC(uXl3oagQq0956^z9y zV9fsaMIZ=^U=`(Ge^YR$Atzq)`cpHne8K#GK5fcqF?vFzQ-2Tf8Tqy3~sHiVXj5A+#r&EBM?HgWnG6sj3>b;?JnMHqe z9S}CxyhhbvGE6XG;VDU36ljP+FBppmUWZVAfTf{hV*ysgcm^Fd*YoW!_)%igNi42v z$^w7uX%07TnpSSRhuuREJwx=s{Dw8R13j$MuJKB=y@yVtKf##&VE)|A1SW^a>(oZM zPmo+pPBzk^?iRctS&xyJ-8@uvMq9CPVS2g@o&{^J)F!v4tF5x7eF)UGaCcS-&2o>! z&FP5wSfalX>!^Tzh(Nr3rcurvOaIFutxlIlUEp)6M zm`f3^RGa?Jr+C>$g#uIpTep!jE6G(zuTIvwOR#BG%fopfp}-OxIm#FCMX>%GP~4XR z#HZBkEa?^IGu64=#D0#0MBSg(YHfB7Cx1s`DqZGowO&32F=p!N<;E_MG++a@ShA=tDoxt!iN})vU}}+cxZ$ zJ<#U)A{!(@Ghu1pK){wfNHgdNko|*MElmy|mv^tbqPOq}?XL3YAj#*#2~qol)Z5uw zU@+BKra+*q#4O#4`-|?|HfegYx!UqpjUhdFq+P3$hgs@%|FT_%6QZ$8iG<%WXnBVJV}%(hC+27CR*ixcI?{b}r%cZ_$fLyz7x zrRI9;I@+u{l#_NaOz42>6c?JEK#*CRCm}B{m=))PaC+&qJM=5JSqco zF{6`O`4Zsk&bG*EWYPiCHYryV@LM-9G{nsRyA=Lml~uBn&}0$<|c9+ouXs0MO@C0s#6{Iz*S6z;MhF2yZFMa4-OF+JV;(rNt#BZF(h|etqGZ zL7Ldu@#gDOv0RS39`yfg5tcw)vk2QFqA9)tai|If>-+bGa@(NIAas>ZhNBk}Let*2 zeNU=ovU91ok>f0vgRxwti&Pi4lk&r3<2^GQ4_FU1r>kbrzr803TdxRm=f*DDYD*Qu z(vYmdvt|!Tr=vY_B9sgOj@IfS%=*ErBnaujta2%;^(`~NyISQq??-^2g~MW6HL*9k z6BnXlq5XI~v6W{k__lwjVk0oIf3lcprgufu%d*j^^W`4Fbd4vp!Mx7h^YX?hKBh}S zX=+q9U#)C(D-BL8Et#eAN{{Ys?1R(E)+=KaOE#xHTc}R%pKJCd;!^8c*V!Lodl3y` zLD-X79*`w`6=T85;anxHgIoF*s<+aEpZ+kMSWcK_oxC!3)@F9r+Ph2H-hasFkbhRD^I9L%{iAQd3BYl zY=P7rj=E9SnxD|L`U}EHV9dte1m;Lu-hZjssf*g%KhubG`^XtmsE|$#4IR?#^iku$ z+>6-K`Z|?;YY(s!=mGCi%aePG>H71kW5u`SJj+c0e3;IVfEb7>tim%37=4fKAu92>1XxeU#gTX4@@$hV0peUF5l|^qA zP>i}rs0WxM8E<)Ts~t=ZsKBcd<*Y2w7?HKkAkFtcYObd$4eek2&_8^9e9VHw0Z4#U zsqo>|(Nfs&jt*(z-(jJlt?^27e+!$0*s=t~DR0|9JS^Oz*5}APFSCDo9?u$|t|bj| ztCJx-B5tueM)Rb|!LBLI`f~n1fTWd>fd7djb&b>Q{5>BO;}JE>KL779Y07^Re;D$bvAxjgFb0F!%rg{s~JpZF=yU$@jV+`e(UL~?GwLWhHwCm z`X!i-;G7SmqTlWx-57d4#mcofli;?q5fHRaWC-}yt$T}ff0t9s zZpiHG9_GXQK$KDK2Y2h-_}Oj3g8S9E&4`=lsb`jg=!j?4nJn3?l$pXr2}`Z1;nH-! zAmyht>2HiQtYCo>QM#qtO8u5$YcIXxGN-(=oCs?zXU9>!=)8CEiY>O5PD5PFe2G#K zo>%3Qf$+0E;6OyO4(wXG^lL8C4#8%luw5z@V&46c-%^9J&6S7r?6c$Jck|n2vDjeJ z>7z;S0~x;bDK`t|jyvc9A>wx(TWx87u#$9)xm1@l{NH|oxd!5nL zr4@I>!J7`O6;Te|xlj1Vs*z1InhtJa*0AQABu{;`gc3GlJ878Z67Z5D$^B(xl1T<=zfkkqg+b2d9nN}2;De9 z%7S@M*~@Y?j`!_=R>l2NF^K+GP9QSAhsV(KtKL;m!7?{DsYC`US21IT0&YD0(k`7E zv|{XjIVN2f2R7t!;1G$oFV#o&wIOY@tDM5@ezrtdm`xu8IJT+p^&PWPJd?Mfhj#=pK-s!4mo6V9GuPxYGbdG3sBJ%|B?K zAEzmA&6nlK`2d5k`{;-K1VC+#3^^P6*reB&%B9-QYWC6w8L#8*_+#Kb3D>2e3M~)ZZ1pD5~eQp z8hm|sHwQ#Ye$>oBV7YF)4%5*3@D#7~Ise{daA8t=^wS5$MVT}n&UNpnyCvbMClp+! z^_;9Vf&AjvYfI+o%_}oPOdcNJ`cK!8bX~ulEuQaJeEpniS5_kPKQNgLDv4=XG1S*; zMM}a#T4bC@c(@sPc}a-xXow`FrIq@bLm)&dI?++8DJ}{^>B-2rbzi-2hc{7YWoKPX ztcJOa?FV9_y1>|dUJ;ns*gt6w-{&xoHL!>=Y2UuIM58V=Gpz*K5fk4#|M&~VBHhot zTbOi2j_EK60~7saKJkl$H&qvaJx?N#kg{BAeei_FDlq~teX<7$pO=Rxsj0qR6Beh& z=3-;ZI$TL-I~pV>2PkyQzSUyWUHD|g$0^7ck-p_c0>gC9&*{m18CEoYfeoJ_fe;-X z&3ZZn!U&V?B`V8|VxN)xgd>PR?#+zG!vBrexO3z|=c}uzfohu!!%;Rq#n6y~5%sR7 z62kF{u@GFe6MssTyL@t1vXCw{Qp`+y^my_R!<*EUm>jABUMB0D@e++XuISCTJtOS} zA(t^qXz~9tlc3$nk~?rt>Az!O*d*@@*klrKs)Gn5ixMu@RkJ@t8rQ}!) zdvunmaU-hf%kn3o`5~pxq&9PqnkV9i(}^h1>+>8JVhiND%`|Jdqp<%}4CQNSv9|9? zCe>T2pOSwIppS7lgL5z{KkY7i;{;hrmwq23IFy)2UzX0|dDLO8)mf3Bly}5m<6R#_ z9sARWA=e=4%R&nJDXWjUYyjLp=+Kro0iTxU{RnSXi|fBU0U+mKM5o>CQq!{vl>02HiZXylCQ%zylRYS}x|Ba7SoymItSCzngtAWLC(e>kY)dyX~U6ZbAx*0zcNID46_6IfG@R7gDlYgD<5 zYlWP_|NdXvENI01UNI|T#(r|Pusp0a`A|=f90hEJ1LGFg6Cno^n(H;Hat#iDnOV8& zwtMY^BcO~Hp}$DctfBxH;bCmBdxmgs>Ao{GIfvA{?7!Ci$~qP56@vKEBXH7^*8xYP zVcpQ=87^a;cQSffb^yUAU69Q%5kHq=QKLTN+F&yEYWKZq62vPLCpRy#lwMtowwKiR z85=`ltDwDv4q;w^t(o;!m$O8r^@Gv<+0vv?slJYwf$9jjb+={KxxU5aar~mTYj;Iw zzKmU~X`|f&JO(EnK*zUK7k;@1v53fMSdmx@l;$Srhcjd4_1q|+nkld^Mi@WjX+@vPYZOi2i4<<3#e3n1+;Og=%h~@h zbei7;TT5x8gXz0pCvkAX zsg74SR}x7*hI{-i^KP5dTkiJ5Ds6YwbMcQr&PftO3tUSX zhg-0Yo?=0QJ8=Cd;JsK~pm8=JRxpO8)x}o{{jN$kSNePlm7aLPb39o8L{oicZ>1M@ zrSpBWE=PcVaPBJpePEwgL83^LEfGyXI$!znbX8lg&Bz}9Mfbg<(EV4@fF#q|@Yt$2 zndFBfqZ64o0DH}|gy7cX4m=?7>-P?luTr)sRva55HxsotsUs`PPz(4-+Url1h_7SQ z*K72bY}!0KM0-NMWo8qXHr-^Ij@oAhZ^AM*3-j}hKk93&7H%tG_N@9(bF1R;AmzoD zG50Hk^CQ>R@|z3%tNZCmyUO8`jDe)jluR-y&205YyT7bO>Rz9RE=(DlCNZEad0qd; zIa(%GF68QZW7+t$QR{Yb`ln_an@1YKM&B<3-hrBoJWToJTTBLRs$5b{@B6-l>Yy*O;DWVz)CHjD*}W7+5+E<{Sr%&y_7qaoyf${Ui{l0X&^s#2^3uN2fxaon+v*;x{EyeLcJkl!c2qh zE$887mYCO=#T~6}?hd(YGJK?;!6X%rjJQ1_l7`w8lh-W&++*q`>b=IaN2OKO$PEkL zZ+5;C`hF)n=AyYt`?;uE{o39Nn%-oJhOmKX!}*F+#!2>>w>uI zlnF?4kpESZrN)b3(2WKi{!8O%qg{IR`3FU+0q#nG1&IT==dnELxldN}^$*n4)H}J@ z0Ms%z9ViB&BmmV^x2T)5&B3`0SeAR+`5$Fk*h2*ZDJh_Cf&7mn1q|Ev)$g^F>3`-6 zkigytOwPsev56N9ZnJJ^4674;{KsYkS<>g`f5rF!JH)pxv;Ma}SXgNGSE{Nln?gFN z86-^;`Gg5PG4PcCbxx0eg+7xd8vGQ0yCO6DUi`(Q0dFl974~u;KYH+`i_F?%tW>Dd zwgQVAcu{|Kh`obhy*d9V1H}=^j0443YF~YcRP^VDto5goEj}0y0)W2gxw^ zDS!fi^bGd&jQsfV!v=t21qvA%}x&JmPY1W8FLrn=Qh z1Uct!fn_jl`q{tCmI9cJ<-}>d`On}jNd z$3W7yB!YBRFt#6n+KKH0$iTv$Ay}~fSE=W`Ne&PRc64%T6*JqwVrAgF0d&|xO_C-z z?HVkCv+N>_f0zI>xAwcB5Fi*87zs>4fGY7W2Pl4yq=${4f~BO!`*`^PAsN&8dfPi< zUbk_IEBvqTAu0g8jHG~E^zrWobKbNwQ?)e#sO7C&UjRuOnRpc`V6?P>3$32G{FT%x za(yYmqOHcF{l=^HSW18d5XUTB2Wp*)B=Z0}@fnQL4};|ah6h9i*n`br`+h(3U&*=$ zC?HdS5BfqQzr5CwGg(?uVQb*p<^N7_L*T=^kH$tuyFVUP`W9s9Bf8gqbXAo<%tN0WGYw9E<^K!>z{58>W;6?B^tUjOfXkCKFlmvP`7?Bz35ZT z1IC-(Vw%zV@&0BHoSX~Whvbw6fCgy*YPp*Oj((&uYX$Dp3Z(M^1qCI$kL>U5Z3sUi zzyMi)fC-+2$%6V<>aJKE9T5OMQN4RT4e_HhH4+$?ogFr$W~xOvAjl9()j?&yZMi|j z<~_Zc?UeVmMaTP|22xLY_46%{Z)EkVwT6fa65ja8w}f(&YrIPu@YkhzBCLOF??l2& z{5R#Bb*Xix1i7X(PL64%=R#VTy7DX_$~-|I9Sk0C{O457eJ8C$zr%$Dx@CwOhFgB; zhzPe2oq5S|O4OcH-6lox#T^lQ`pprepAKZ}S-psMoBKh`Eshf6qlHb=bwazXuHujw z_p&%D1jA~hFSZ}nc^90-z{jU;+!cyD2hck6=JNoP5B=snqW2GAX|IwE(qP2&C@CpP z?=3W4ZJdU`0d{Y6@cd@h@g|1&3!`7}LiShwZ>lyX{sqfkyPSSGnEaSy_L?fw%$W$9 zORnz&EKg`J-TD+#*-&)1`kds3rC68X;?dBN%;U8K7V3WY^VUMKPFdmUm)1k>S~{5R zq-etQLzB;?l?q*^gKiZ z=8-Dq=g*%Hu&A$>>nV}h_z{oIV)~?1tEn;0P!m{=@A`;gMa62wq`b_`D9y!ButNpG zzH}V8jlZMwQ_665#(y2Epw+R+^^H{7M>*P2WB4{YF7LKlW%TEXD-(J7iF6o0!M|gi z(N$@pPS~UR=7!0uTBFY>eDQ!v@A24Te$3l;DK);qoQWK+CGnLSlyi!dLPQ)L4sH{~ za4CKVAypxb=aHF$B|)!ExT#TcJX_wNQ|zytxY)HUIG(QeHi2IxH1jqUe-(jTicV>$z?C8X6kJ(4gtfv;deGW#rer=uK zxQ&hUt6`by@ww;f2^9T+KKTQ76Y7it|fMwBphd{tW4Y?Q5Pa7%>#p`KHealM}yF1S0Sc>(+0id0Wo5db18ltOn1|JEMq} z-bHHJC$4_%$Ql&VlW|7Rm@Z^A%z@KPIQUY0+FQXbAu&?ifHGcG3Rg&rM=JPr`El*p zb=tYP^}m&Eem6Qn37fxqk*^?`Jyq%5-7Z^{QSGAt&efMC`L6X6p>g^fJv&h?!QG1& zJ#Aqlzr(fs@^D#L)_!#J*WaRPzuYLsRqqF5NUjyDw(|6=g=`H6&V zh0yG>3disOsZ=48yfLtrAJR9outYAwOY{k9rIvfa=l4>igY3%sT`cX#%Vv?MyW?l5eP$dO;3ZuC>+q*`lfEpWiR8@ZFn zq`LKmH_)~7i;HY-r+pW(FR*z}YI%9O{HI8Kdu#%}(XsFilB~ZEh%jHz7>;gG^)u4g z%0?5;_5}Hpu+@LmUaDH9Jxn;HQf#oSTn%|$Ml;LML#;2(32OnK%@<#;HVN2eNmV}E z>h>OvGB$+-ee5c>{eMcg%=o^OK0pLxy$f8@9IZLssK66+LA$z-^5- z#K`?kx?`>TkuxbhqXD=4WXb&9MKQIXUzd=su2e<8Se8-9t&NESWxi>Cj6M%L=Nq;s zdt5e6M6|62jV6|FYD0~m(L^4d(>BD{vBn=M!S{4HhIHB#*RjMFbBt(7iPX=Z=2WIn zm>Xhr@H71hn;gAv8NEe4ZD*zwf&&Hc`M@I5zj_K%m#ryLZ14KeiCg;=_$qHV@!<~Y zE&@cmEZ*gf`)3Tfx0CVgnKvj1zIVy!%;x#sgV3{`YEo0NL=kF zDDk?tt!1lBvAG_vUBjtza}fI3&I1d0X-~(kD7ziTDkaNTB+BQ{(EMw*sE-^;=!-_x z?ugE>$&jz>_y-Qn)YmUmEHLo9=yzYzh)uvN84bN&``)2DP|tikay0db0XPVy7g;^_ z^M<_feQ>Zi0T!&RLv%=;VF>pb3#Y?P8~_6pOBwufSR3KO%-;)g;0cYK|Nf@L09?51+4^!xj-xhxRTI>_|qTG`(r6*6ufZORP~z`rFXcviwq z^kT^X??2KiM8i+Ft4Ny1*UzK0Ng5_4gsQ$-W+rCSH@iHdIM9Z**A`OCA+sT;FTs`0 z&my39R6#%BVJX{ObUrH|{rW;0X?eq!;dq`Mw|Gy<_fSSu*Gh+QC=~~pu#xb^aD94j z3q2h`Wx@TfE~dTT2S1+wyUvT3TJDp+4UVUID&K`xMV@vee}8ZL>F6`HQn%7FKDSuU z@74ZojQ^BQ+qExntTmsGw)~2n?`KEG^FN4$*KbA{ZGU;Xc+@lJ7wrAs z^i~(xv%UIeYW4>IC-)C@x1gmLy3nMl8Gcb4*S7GjB_y}+NsNSV2_F%;CI&K$CDC=6 zW*9BBWnC^wF@`)OVhcRhdGkCeoPBZWu|5Y4$wthj3vXZ|wSNf+=OxIfsG*>p=XiJj z;Pg9}VaIMzBVkn&J5l~_Q4%8%rXDl3)SNP<1Fj~3K4%Lpc!^A16mYSK)S97Z_E#^k2Dg)N=p3_=y1~_)&Yt>y4=O z)$L5)E?h#b9;kTV8i87rqTEEv5~um3%Eh}o)Z(=o9r=jO@lqAzmP@2d5M6i6uZK&U zk$%jhQ=V~xklQM`WFBEQerGmkfgaGG5(k#)W+oSOD4wVu8|8XmY@ z1<{*^I1g8jm2X>2dL7miARm=pK9@##WhCwR-B;GS&&J{m&B!?F{UCoS&E`xG&bsO)*Nn5wUqJ7vP?1x^0t0YaP^#z-?u0{@m-IQGed+vtav z0mT?5GaHQ8!OXtM36)-;Vmw_03$4&}(c4plm~)Jjem>Rh}#q^;I1^)oS-rlXInSETA>>FR_iPt$&Ga06@mC&xXQS z|FnEh5U)SFDT*^oi6?Fj#}I5PM7FWgAk6oDZr4)X_pr)r3(ykih$5!c?;_FH5Oj7Z zg;mzRA>g$!{aUnNR>lcK!+KVQ>1eqrK4Vu9fkS);dE{wdBkS+2Od~3xe55pr_it*0@M7J^ zJiz+k{NfDeTnhf|^h;&sLb~vSy2`JdSOYW4wu>6#x-@RUy9G?@!^8Q3G<`-#NWTkVp~lx(veL@2Zk#LHuX;0_!n(O1v;>v8 zkyt_#B(q-PzU-Tmn02O`ePodGMQ7U?t8>}n-Zc~!CloOy)=E_B>~)l|7@k2`aSs6) z6*Fjmbr)bSM(eM9K;3QZuYtLR9qSg6IY$OpcPHlST1fEIHcdU}gMubBMKktKL!%u> zcZImm*-8-0y$iG~LC^P2v(0fVL52L^9k21z(5G@5AfaEVNBOZHl#j%h&zHX5ONEEA zJB?_fGUt(Uz)x#a`w<7?-K)^I`c^TYm6i+Y9>&S{RcSR;2@kPrx2BG*#w_KNAE_ox zpsidE+@FvWbXhkZ%g|*i;)9D#F%{n%&lDfSx_58oH;80|_3%X_wkJcU>*T`rX7+eq zGj`nR4|mo(Kn=zaUclj-F71e)cst*B^>@6a>t`aECaUs&7Lqks(}J-rI((PJ>yc0b zUcAEm$p|aON93_`VK2JQB{IaOLzLLqts~3 zcWXC37!N&~V(fWfMY&yMd}CBA+a&iDNjf7vW8FZ-gZ>geNgwBl0OpapNRZ^wg=4gxFUl2r@=hd-KPrO zGA)CtoAkl8_VfUp9vl`b9PVaV-=^E<^*L}!f6)<9+93vW-au=0upN~gN|2^mTAHhe zkmg0J$@CG@B+Qyt!mCBXX=m0$<4&&Kks<-K008Qn007yu!-Z-uQ)SeITa8T!~|9jmVqUC03Q6{^Z#g7^c%0OKOC2VQxggce+F?* z<-PkyUPGk8XgM|-pBi;TV37gEG%ceD-s$JNebKZ}T(;xS%8pVPaUUx9CSC`=Y}l#H z4A83-rc))RQpUVvoK_sG!m-7H%a<;JF}R3YpYV&vo{G%c1Ud?0Yio??4-1xmGBm)A z66o7OlHcs_X_2Qqs=rG4bmx|khjL|>Z)Hz6%E=2KoG4li;ioRH zz}v(RjyJRrTTGYq1TeC5!H+ih#H{!V9BQ^qfKyI?Lp%fjPM4rHk^R0Hcl+ZMJeu68 zuP3<^5gSGlmv2TE3HJ*a6+2C&6)EQ>S=vVGWm=k>cSoj#GC$k_NBF|h@9`3690L{p z&y3#HJKPs1#%%;Z?(Wb5n}fE{kc1?hfziK$ zm86EtAHcrK zLa?!gmVyY|I>ATF{OcY6^1H+t8Q-c@9#ts4!tal0^42C@a|K8)-1G9u;lp>h5T;Ey z?@#NKANj}%JTqp;9-J5vWZYU!#8xhl1l|1*kUm&2qmGoBf=SehT1-6Zi*wha)i+3b z__CuwMSV?|p5hNr9DHt|hG3m#@YOwS)q^Y8=Nn@dBqm?X+_FEK{P4WKarg^kQSReH zO(}eJWsXDht=HID!P=BCpge8on*mqTZTk6C)eTL~+1?8&b3LQV`Oje#m&FjxE4oLG znKoST6aoIgHXCCtBI&(9)GA*1H~v!b7wc3{a=`^H-5kh(4(&CZ2$-?72mdB@XN~rd zJcbOPJ+0aX_!)mhbY;aF z9HR8~n}&Da=Rk_cNFK1z5#vAi@9~7uWDwJol7+$=%5Tq{_bM8pX}_USDlfwVsy%-c zJq)Fq3{B_g^47=DP4zn6{Z{qlwQ^)T{t-CfO>Oc0@oMw2?28#;%1ny8;byA9koR5swjQ|;l$JU}f3*;5p9dZ}##@5BA6X5o^zW`V z;avi9w7P%km;a8ukfRbzKr7zUCH0Ql<>`pe9mz1tavE2yMj)DZI=R^Wun1Wqy$a!* zYlvPckGXW}=AE84gLiFmthG4Yn7Ijuc=Lc$%)JSjTvwMdRP&;ho(ZkWV&|*)OwF&x zJp=v!Q(&-ke^X$HErF9|3nLpE|B%X!OKny(b2`s~!MF*3CloXor1RYtl7bf0ABayx zgo|M{tm;r$jsg0?78E3gN;)mVJwG}CpIpqtOIrdjyJHJ3gkzN&QO*31+y;!BhkC|V z^DL}p+*6t@YQR#!5-NoPr6SOP{kuM5FTmXt3rtEs@N@y;a7!4g0f4vwvz8ZnueJ`H z(@z*us>;cKD2kQA1BRT<_(8Gq8Je!9<>nxal{Ql_m-lcc3JNi8XJZ<)_Mp6@d;XTM z__31wsA%AV*GKD99M1k0?$9-U%j62fW1~xcF`8TlFVScA%(SmsA9zwe9*n zXB>+cd!p!f+_v>T?{=R&OYqXFA7j5WOUp}Ov`rKxp+wg{md|KS<(g?_3%nl@_qZ23 z;fEQ3cyQ>N^lB~F&!oZt6@5%1arK$tXud4o&n?3Hsm2BNyL5n!`(g@x&W=oO$0~d` z{vyj$>-@D@>$Q48swIK2*-aUbe$w%Kg;M(PKw?@P3Ic_F>jw?F#NkV{oHy4F?L_}G^%$ne?h1tkn(ibmV?5^&GM{IRDR&1A0_F;0mnP*Vjq@E>mdO*#VRTaR9SHo1zUco$<~-Uw<%l$BkOh3+v4|> z1E=(|tWQ4?`DFMoU10q^g%H|0Gi34bDarKJMI479>kDYDx86H&XR-w!sDD0?n}X2u zMdb6!T%WzZE#Gc0yLY5R8aLnja_&}b&G?Qv4c45qS$n>)DO{n>(C%1SUY^Ye7mY7h zHxBJ1skv`wBk#@zZx~h@esQ_Vveh0-4c1j9>2czuh4m{={r+PSw2w8%vX%u4g@Ng) z$BJxY$B18s#ksX;=yuCVW0$-ASFQam!8L;{@TDyj2b)JiO7&L^w|=Co=Dw@RTCuZb z77UUXc}UE_QAt-*;)$GIKiUa8*PCf{yHjU)yspXZ)VeeL?KbK`dBG3Cn3gcJ)YiTd z;=#PiSM!(KrteIa91zraNdC08){xh5gWD5u1 z#*bh3w|;O1hC&i}y9ps7A<2s%+h6TX_?eiRy1s3LI{~9Ev(V7cQxqI5VDmKKhKlX$ z!9{3W>*ZI2pvI*eN^Z)73(YuMLb%YQMyiuv=l@X-;AmP8&L49BQic`wU3D52@jYk5 zjm**j`GxVjU~J5A`_4zzbXRioV=Xro@?`ESrKJHNc4s(#vAAh>wN#9@pmE3GW=%tP z@p(yWd3 zN}VK6I6@AGx%4&pZR@vz$1>Kwi;)m#TH9e&{;eUcpO|fAt9mzOY~*;{wcLX9w@%uN z(mVC&w>5HT(J)qia9FizuHVZ52qAAAJdt;}l_|japgESpVV^L(#Adw{9mUh*jVNPT zj#vjkAmWt$0r1hcLz=iwH6XHQUz8g!A*R_g#jUzO?y-e(QwSbP^ZARCp>DCWvVv9c z{;2uepAEMvPDiu{bSnGisC*iLd`=j=Z>kfa?b;_OfO(Y>mM~!b< zvH+h2;9?=b$4@6Qju7*p22uz<;TdTu|Dx~OZ~u__VvsJs>tvwc5yi8St#hR2hN61f zSI|Js2bCok-ULW-YTJm?6J&p_s;fgdS*SVuVOMmb0z7Z#H}Y=zblN!JsS4KW8K0^) zuuGKc9$@nUb87N79G40-=C0JdRb z&M(9OKB30nAF2o-24dE(J1F&$Gm|NZZnGXsu%vitq~l=?-c3%ufY~?92+tK7A_;ug zN@AwLU`BwrpnWn%UDRZg?`(Dg!f`WP$?i3^1xfVx`PRdr>>&Tm>SUY%1DcweV(QkZ z$PHEkw&d02e?$~|_+YQPJst=4_X}SxO{H?XE`%@RvRQ3K0b7pjdTAsmFh2lh)6|T- zyf`_98e~3ydW}{XyTps@Skv27k{Z;WWkD5B$o2r3_(xosOt9w9epoln7WbB8V}kY% z<^y3|zYh4-SB6%5TEY7e`xRZviM1H`Sx& zEX)-8urFEL*|r(pqCc$UO26uH<=JgViNWprEMoLAoaFJ|IbqUVlR{GYltRA$`e0U+ zE#w*i#jv#M{`#Dgl(bkiJuP>fx%0Z4=AL`7x0fCL!_*rw^V970cs&jCahMBwBGQKE z$FX{n9qOkqwVs`E>PS}1g#9DYX!f7SaQ9~t=!a|`S3=Tm^xY}sRk;cdo@`RCYy$p29Pm8ygz{H0p^MV8^-3DZdvC!K^7#tusR(ETooUQ*X2>7y?j+ zk3-m}lQ~1gwjh4;FeZ1r#O>|0jb4@MO2oKzEX*!{1mRVl-uN+hLEaQfFwUD125P2< z52MMpN7idfEfHN1jmI+T+u4N_MgQJCz}iIx3-IH>4Nauv>9P-WG_h|1Vyn)fp&~Q4 zDe<2_b(HW%^wiM{TyVKv=6=2TwJN;qZ~eqMJ}J^?yAO|-GP=}WQMq~{AbMtBW`yu& zgzk>bKI!r8(F@WaM#IV{b@H}oa0jaHX}%C?54I#ZrN#es;?lUbCEo|ZGo-#foVLA^ z)R->bkn~wLFHOyZ-vA8`-;Ty;xtcyllg;(9we;v&%Q4 z@!ifSPBG)m!R$^~2DWVqP{f2uY<>Rk4a>yN@mvCQuS0ZPqX(b(sjNi|--hMK0QLtU zKV5at`-L~wxg7AB)J#TE9dL`$hxVO~fl(UYCvrSJGb-rjXk;xbD!y5g)_ui0KWEV3 zfBW(x%LJt#052R~$fxL+mKvQJ&8~lV`$GfWJ3KIWlU2Cea2#<`EG0ZlFWm>!&<7lM ztK((w4FEDTRa%YAQA8uaqXf=$1wcMQG`l{|CUX6zvQ;i0+P%yLW`5(J0fC2lEI{k2c&T&xcg(%Mfh;e)Q`T1=Fw+yepwQmaC z8F~P6sNTlbHmXpu%&iSD&9)(2^d@xf0~L>G3ZS!I@#b;pT<*?XkIwB^U2mNMO0kw{ zz%)3Ij*c{{{$5IAdO;mBRH2iH)s{tq^>aLdO(*YVPVY4}P#H3mb=&QJAsTcn#I>Ep=9+cJQx1|g0 zDo_=30wIWUSX@G47Kh>YFubJX*uykT%3DOon48xHc%|xOId5)mm;ux(b9r5zq?Eq8 zw;c0!;SXv8m>a=`3y<)^-vDvj6cDP}{jCnFm5>kcw6#bhh;}gWE}4ayQJR-Rj@{SK zPp2lj^GOz1N__j!SGN-Ka>cyjbktp=>eFqeCQFm#sDc2zp97!}&l?dDu|vOI>GKo7 zVa)>iokE}@#Z#X~cM*y|KfT}tZlFS$2UTHMhm2?469;boi z{kMoMj|u>2(W%vlB80(!+u1-gmDB9gK-rDMXfUn~@a9FoxslA-dhc); ze_I0@J+DSsg6v`-%`0^}v9v#3sR7_bO2B_>naFPRHJda1Ij!By%B6Iq84+JG@|6aN z^ry>BAkUOg6M*AEoR=C%u5{d+VyV^H%bH){bf0evn;5)%{eTL9ADY|f~`7dzs;!$TnhLqq6k^-lzP`!hv~I~p1qBn^FHM93=u|Bg7& zv^~rSZG@6$K%1@{SNPf#^9!QUs<^{b>f6t#=d^S92ELU{DG-czIC;*GAhg>#;OK?7 zFGMu!o~_;O)Kf6^vPomQ^@6ul*IlG&8|Mu->gYz!x=NctriIJa7)v2{Ymv_ukJ19E z`_wfD_iaH8wK&sdFBb+mDOYJ9@P?G>}~0OevvWt-l5aha4Yx`41U0F9j5 z^UPImc9?i+I&EDfayIuLtR&%^4t{RKM}_ezP*G8#t9&dj+q*K+rY1#%n!LWcTB!(E zOhAXo0Y2}mEN2YTI{)U#6(VbbmkY2K1)Y;|C+Mr0aYRWUFRFSUS7Q` zIbl?3jZQTfVE7dSZmdrHSVbRA1Byqf9QihH4AtMlqRn!%Hw>U*{1R>+gWV@4 zDmtlKV142?GRIhJ=x& zU$|`B$(#=-FWAk$WQ_?va-SEcrXqjCtJ6J~voVv$(p?1goE&N>o$LnuDL24S<>(lx zfdH#~1?L(o>8p2q-B#B08|61^hGDP)_)Ek*sIZ~#F%ICZ$-0TXp$b0z zdiAqM3=r`7Ee0em$MgV?We_C=c@bb!EC7}g8rEEoKM=5{aMxK>y)n@o88XeA6(sT& z4ZYuXK%mp%*S`G4jDxoA=2eN$!o)O#Pf0oa3=kilR3erot1D3&rF0hQ2ZWqwe&mFLh?1WjLhywW7>BRT3Dkp59f&tun}ACMGBpP&vgF9&PF38$Pf^)NtVRrQIE4YZwAN~?t7N6BEr^di zFXJQT5i@-<28Utp#OQ6tZiuXo`9*sUdP8!JZA~3qQrHE&gRZpudUEJx?h>YqV`0xfbkX<+I{ z!H6qF`X5Xi zuTzX$ZU;G(KJd9(pIo)B92>JGJ?0nL$=DXK|8r0i=MJ29I|TyFlqB z7Ag%xR1bhKW?BgI&s~71Y*V+aJZ-?N|6SI%Jp`v z1V8S#<22FA7sKHG;~E;UD3JIrKuz|iQ}it}z)!?P$ncsO85P=ziUz_z(w3H#q$I9# zV)uOs=+R*)kRu&YLWbLgsn6V8!&58QX8xk@v%U$^t9^I$nG zT)aQ3m_X{DCapN!>hb8F@x|n{IynKV@(C>npgr>d zqBn4nzFh%~k=g$q&T_5kVm+`}ArYs{0jvU<=BN(`p-Wzsa>D!Ox~0C@UXIgEcd@{U zQ^@Bv4Ga{VIix{Y6RJ-$%?@LpenAO%T@Hv?O*8(j;@k1Xk3q8Q?3;$l5%E!R2;FGU zcORyh*2$%|?Nf~34v1&)yInyM>gQk>piT(8#O4pi)JF{{S5MH=HFJu$N2H;cRnKQQ zuQ%W$m!LpRedSp*^zzGb5AEdxWiINW2dDxQrT8IgqoGbUkMH=}`fNu4btKVWFAKE~ zDqDp%iV^cS`i8nCC?KH3<#JaJ-6a2mE>%O)wvG#$Y_>VqP9a`-z;lW4Rzv$@8RN=a z7S?$oNk61aTic(v)aCxU(N}NL|6#a_W$uc-W$v=h29$5XYi39u))#&b<>2?q6-whI zn2M;LSdL%!iwXVeX($S)49)aAyh=)ru>fB!3K@r;$&sl)_F4Z~2kTexdPi@8nG}wG z(>o6$AH1^-!xTj%OJIPqG((Hxk$j$aM-P)J)F0k!ZtJ9bG5o@v7wRN zmJ2t5G%HFnWt-;m0)}o?4a7E)j!EMy8d5PYSy(Haj__Lqt(|sZ#=;(@mSWo{*z>S68DB*+l#{ns>Us9TjKb@ zz`ow)f*=jb)GZbEMOR)B0+se{h^mAOvZKDPE^qDj$55JMY2)@={cXbPB^ejwG!@Y7 z_fV3P3EnWH;s@QCV%^=+>k9;UYs8_IhT70lUVx{EgKc+lpF)lDy3-1z9;w&aVm|w# zyPc?C!H@VhTi(l-iV#$rKo>he2fh52|&{wTg5Bi z6YX8)15b^6+xHAccGr1p@zhMP@I|j0iR%<8I6-lBpe>}%c+1~$o=>ez5edVY9%>~Z z68mrp1%@uK3AWW-E1D4M@(ix)j)A}-CbFtcNte*|iKQR4rG&>@Ewv-!Zq+L|HsE2$ zSILd$olw=S+Y2q2}$2^JyB;#_YdnP;vF~< zS+Xvey$iJUg0G*|$2&MxHB07=tSu_LTdiB~mPzSUH;{tI*g>5OJH&^zd>TbUy${F6 z@mdcel1rg+d|lHhtKU4;Bs|?|9(7;546?Tl;OD26@Kk-Lc@=}gij>LG)_12`m5yWn zEaC7Q7(vr1+6C{Jpqp{`^2kJ89of7uSKx<=x&QoyQe)W}5?UhlOAdR{{G`u?4rzk z`RiQHd(f!av0h$ZnEcNjnN=gngPE7T+@cq*N)V-p$T<9&i&PvON9{3&^W9`jD%U|G z5A{C%Tj+j^W~ZyKtTjN^%QR>jw6rlfJ<;QnIU{lj30bh6cl3eWT{i_D+No?x&Ga z;9fo$wrdO&G-=Ol(R%~gu6Kk*hM9RuRIJMw#kZqSrE13U+VNhvCdDwP#?0I#@UC`` zU-3h@#Bf!n8TqlS2|jdfT6|@NCP1pdXzs&`Bc1h2G7}V6nK$CFdL$l&%*o7YLCVyP zT*FGEHviJNEsD=wU>Zy<2nbE4=bURN=-%=(@Tqar2&Fq5OVj>d4U5G~I#B41KlttG zz31?BPYW|q5rAQTM9fJ#>npOuM_h29ZqmxrjJ)-jBTfZtQyYOZbY+u#>E`!1@%71R z5Rz#GOE=%P1AR*-cXZI^lUQc{ZrPFZw^dMt?|1Sf6`#iwd~n10iYYkLqsZJr4Lr3A zvE|kgtmP+{gK782+XK5sz0C)NYwssHc~g$-5u7C6uGXbnc3z!f+@@+=n^(WGzGC`l zLG8mdvD`F=vqE{RZrGp*exAj{>!;wR6lR)MqM`1`6kA+QvzMD-L`DCJOU05qZN4;j zaA<-?cbC^08mv*yA{N?)7xP|e%4K^}`C77;Xr1}Pf zJDP+^Je$Fm?_V(F&;!glq=s0g$9mR_21)wW&x ztdv{_Kl5;kh1cCI-4P)Z7RvL39%iJ(@dsc!jO1CmmY9F2e+%qtG%ln6_+3j%72U*G zKQ+NhX@3qUjnfxIu@z3UjKfx?eQ>#sV$3?Y@X~ zI;(waqS+{3ly}6%30>QYUmj(&eg*S;8Fk`Y<1j3X)81Se?;XMth-&AZprdS&E53QA z!A;cjC!Q0oYzLeX%;){txy?M%=#I}{QG?}+CJ@~3O%~f$O&jQ9xFp>03~GO$*YOl9 z2jekT!%bl_8ouP4Ho>zrT_3sDIxh->YQUN|G&?GwoE4twM@SXEx&w+GH4g3=$#N=;3P3UoY#*H1aGiGzGDpe>OGl z#}!<9U%abQP_Qbv>v`pBc--t*!ofKI1iqWQUjk*d$u~pb6hTV zUaz~0rpQ>`CL%&xE>iSck(815Lq;uxjA-Rg!(++6p-bi*hr`a~&%bH8>)n}l6p&E4 zjFjG#9^l`e58YN~q!+j_7LR$V_2}Y~*cApa@Jd=xi+Zj~w)~MxwtTorpOwJSE{drk zic9sJDx+_+hOwc|J@G?}ik;nIy(l2rn9Fo|Kgl)^k4(DhVj!rQ;=;vcGcfBpwKNDe zq8Fqo`i>f`n{Q!>NYc8)46c}ge<_%~uU>T{Pyb8aBadU$AGvvE8&QiFj=0potoI4( z1Q#{5Ed|Q8rk8XT0jOv^lJAP1D%}}n*p;^CB{*1AuPTQfxLw2vdS_WWDuzzC!@4$p z45G(~wJglg7KM5=GnMA{`njIzzCzs5jV*D`VGV`KLW!;B_zOoKXShgg)0mAoT?9Fb z;0J_t6&Gn^oEio9#PTPLET|VpbdAw&aK}}59n6T)>MlXL< zO5WRC;SU5MsggSzZ$%y~HC%23P4y8IV3=1DrtyUg&p~C&1ao#o-SMZ7pYBjfPH51ZRT$Eiciv9P0=hT|WQ#+w$Yxitp&vs_VD zJSCBoZ(R_eV*FtIly`y&RZu)VQNvfEBgSP47Y_vsV|q6}z!Xqc;A88WGOpvtlS$9N zWHdurURdK83Mcwm=A68WLeis^`KU@$B(P6NOehNbP+aoo4}%?%tjTN#R;rd1^B<>~ zvWR^IMI7&Z8e<#TaIvrs+trxIROg#B1&}g-Z63im#QH?^=B???UXJrp9VcTk)6RI* zn8^8?a1>Y;Kt(OH?-rIs)M$jd%8I}8ucc5w*SmNb8tR9ic1WNHoQ)kz9bJTYxEDhe zOg3MlR9|n*40qL&FwdbOd!k4LF57$k`j}o&;L%&_yLX}!cn&ul)Vwy|H?53)!0sDR zpfextF3v}FE<~d0D8hZgB{zPUpPk5TdeGr3qee`IaK|3`BI~v=?LK*{`jon4+hntC zw7Y13Qrl>Ms{#AUB0Ymz)dE|8%9`We=Oxm23ip_6pVEa_B_eSpy11)X=(S-93`|_C zM>{q#Yxgblv~Tn#Ic;M!NVW?!zUQ+ZwgoQ3`VOzQyt{ZaH=R`={223Q7Df$<{DgSnkD1yNl z6+*wgfwC)`UWPwhwK;xo%C4p&w;)2scy?;myY-#_;HM=e3aJq_=x|2$z+$^V7md_? z$HEv3JnC3t#P(%Aj#a4u(rEz|?#&wO8vHG22Rw`9X`{#v+_rNgz$ zs@kqp{*@tv0i&sY<)n1RDf%yj$1WNglQu3@uWZbIzmKS#VLw(^Px?yPQ8x2sozv!? zF{B2mR;ik*s$r6JM{=y!qa+fU-&3OHZE8qWyC}JyQo)B71~K=7H-mFmj&4YS=uEbL zd1%a3U#5_ZPf=Y<81US1MV}t-5fYJ*9Q5_|VO1*l0XjqhIgM7CZZ6`6CB4U~<-99n zAF`Wi+ek;9nM+d39ky@g&-W4eBJuC^I$qJl_iHxmscFWp5rb&hq$#95i%H%nW(LTW zNtl%5zf!%tf`Ha)Zs6mrxDaQH1*K(sw&5j+%LO%IUM8WdN$d2$ug*D)z4AV&fsJtx3$@Y)m-(hnbXMph@85$93@9A9bp zf+qEd&TlOoYWR}2r*Vfwxg+Ac>gV1{$cW^lL)Su#iAvQ)zD^pCQNMfeMhI56RY%MU+3rh?75$#*;pZz6J4b^hGb!M3MS_ITiEJ_X z$aNwRh=`Zwe_GnsQ}S|O=SLmHK4bd{i$hhXHIgUNB{<9#Vf&W-0Ruoy+{XP^8&1sr zSfGTJOp@HWr(c6%eqrpg!Kelka#*AG7*ap$DqnKvRHLJ}m$ONAh%|t~xHxShu9&Zp zm22!ZXkb93@%{y@poDO8olxDffaZpfH-ahl*B2bu2(wzfRyF(WMO#g~r>y?%EeEwM zji@o6GrZl0qm!8(bQ<-2tH+ZTg#PH;c(%*ARy zy1wn_OV_%my20!j)apk|sR(;EE@`Cm+6a@YSxuVGk?kW{@0}Lv z6SI7u0wohMmfOM5&3_EFY6hKm&Ep^K8zVrP;*N5`^!mfDUrcQ}*X9R!rBGca)1Mq; zg{I+QY1z3F3JCV<+=L*&#XU47{swPOoF5Iig$m@gs3iO3fc?`3hE;&`eXGfDpU?VT zdSjkl8&&Tu4o~&IpFRGI}8?!rf|yhY_;M`!gTeaAsdiQRW!YO zXh>@_mdf;*SaCHLj5quRG3_E=;9N(i2vM!hIqpw9!-KA`zs-g{m);IkUgob zZ!KG-#wQ2B;BEn)rDxPUq9XSGmUUDTESMxOzmeG;3zXc=q$ry6*qS_KgN@r%Cav~J zd@+WnvIc09<&ciWGvVPz-9S@Q25OhtGM|s!uoB&|YjVv_I03QlLOV)TH963;w$-CB z(GYc3-35vPtcWC?m+f`=Z4XpRpzQsG5w;@bByBy_AeWE%Zlxr5hs7)}tV!0*A;mZI z*Kk$1pz>+229uPCN?XDHgOVn8{WM#Kc@shpSSY+!E;G*I3NC$(LTR2CZ&FMoXlL&EYlo&!ScUp*;pzq?P9n^D>(%f zDKBdVjfs5i@+v-EYMpv;_55}jMvbu9`to4@*-fx+_ymW2nSTA1EHI+7)mZj?c|4SZ ziwCP_CK8Wgp-y+vfH1DN!vz7s`hF;8(?LyPY=_fqxEK#az3ODDBb7PL%5@Q8_^RP0 zNgdC$j_|o#xVBW~BIaxOOqKbo(?kC(hgkFQFx#QqqSNMOxts*g{Y(!g*JCi(`Q_0$ z1*LZZS>KB)v)hbE(gngGi7n*ah(>Xfl154>??MZ|`|av8YauFy@s__>0;5_1rfG|cs!7|m z-}vxCztkWK&0!(0s`?wFF}O`P{ZqIt>(MBBGE`|MK=X)QCf;j?P!)FdSPKr#9VBDl zID~~&O0rqD&2sCI@Hix)TqwY zgOV@x70Y6&S_^Z+(#-ufGfX)6s0i3WmEDmNcIAV$w>7~b6%Bbfzct;1;zW@54h=FZ zawOI}&?NtuKTiLU?Sm?IzQDC#B_ULx^zjHq3^@*_t59b8RGgt&s=|7~SW~e%#U5`> zfT7PH75;(yU=dSSL&*86Bp2LG4tr-gNFX^HQkB01d3LmG+^$AT_eG&r^XobO*Dt3s zL>I7u^9g(DAz5U(b>+N-x+9(-DbIgREA1BPG-HxktNo9z$+xJG2>OZVBKcR-5D}(? zhoRY?fc)opp3e+qcb*B~;+KOdg<=5rAKrXaC zA50Uq8XNW|^SwFdt#&-kQKNb7EqSEJ&9Ben#2>Rx=h*JfN>Q_%LhkG|1uErUw_7&F zxv|__>~F*mRAyY;F3(Q1M<1T{BB^!dn(Ws+#9>LVo=qlhFp%<&ZS@qJv?~c7>7AI( zkeqMK@1|Ay#ReTp1tV8kURbW(>Lh8v1cE3_Pamxju`Z52d$j(VJ)pAYn1lZKjv(*p zKYd4+av+tOI4Jc9A1qV@Z|L$1b#CiiFD@yq$=*vqEmor|=6{L0<1?MuFNOUPiW<~O|Pjs^GH5>+^KJ^povy~%Qj)PCP3L7QSiEEtvGu+VFhEq+Us z+EOQ5WBdZMm1F%r7Fxl*=2>EuHcy`fq*|AQ*w+hFb>N3U$`&J>)g`)wc!4tG4>2Wg z1A+ps!y#XtlGgrXl;&s8G@h(s=X};OLcYqg>g+7{h05xoGcrGB^A+y(&YDFW*AE&E z0zE|W!VVuWFcNy_IcA(@D%E$B2z$lnRthqnyOJr!*M66B&<&OMda@MNVJGk5JdH|& zx=CtvS0%?Nl`AdW)K5z1!V#piQB>96%n&I=!ihHrzsi$5NaBrOm5*5{Y?!?9(|~N4KEg7nM1$jb%jb~G(^}B3VUngz0lRUhM1RGeY}qp&GCb14S?h=-OHd zqX!MY&E#{Ag9u*E_{t%Y$<6G)F#L92_#3#DkGsG}n!_ZO-P1c`Ild2kH zqYS*Gw_enLrULVZ1H{V)dWx$Z*gQ~I+qa7dh#n!xzTORQ`wss=MXynIZg>`}eLp8#q(U8cX7gnym`h4}0oZpj6Sr0sOJ77~*U zqo^+Z=XMP>A2|n5qWx140Qia=$JM6b$CuL}rm|Qc2H#r)nAAXH9S!c*fa6E5ty`h_ zns1lwj7%puEmMvGz~1MV5kQu#Zj=RJyTJ4; z!(pVyUy6zsi3{`O!Viy*u;%~)yr?4ud2l2?e{w*Tsc;M+w_F$kw?@LRup-6154&b)`80mlXwB_@d3xH(1_$Hss(dey#qT$W!C z+vQZfdll=pgMpD`m5mbnQ1@b5WbY`q)ffh?`j!09F5mztcHsX4ak$N*I8WZTDZ?WH zLC_y9K$9@IK(not6dYJDk+A}n9Pn^oS}`THpkCY0RA!DT`KoIe_jCaGBe~CJeP<=7 z#?E(g3>;T}j6EpWWHYVe&@~yjdI7bzYtB^>vlNIyU;0u~SU*To=JmX`vG37T+LP0h z9^s_CGKIn~ch3}(l=m-?0#tY4{HhB@8N;5pnmJ3D!1o{_u9Iwd2>iGWL}1k(T#+`NY;Tcq!2||xb^F}B zw9Weo*5>&+FQ9z{_|dO1S$=#J3M$t8v7x!#HR;adU4OlB(-BSgES=}ec2`-aGo2L2 zT5FGfqu~Jae;9kqxTw0Y?ORk@LAtwJq;m)fMLdPFU=pk$}Fn&auFOajfhvAcY{duUEYA*%3e~U~k*x2o@owNPRK9MId zYifGre-qIrqXN%?a~p{tXExh(JUrG~@Vr%z^*~>Qtt72dD3#rWP^yPntwLN$Wiq{Y zt0}uL*y{#^1(%&R{l4mZZMI#&+9y_Lp=Pox?3ktIyqy4ma$OJF<=}bwzOFoXbaS+) zz|2JF_Q2(^Y5h^3YK8ajgD0=?YilHO{&WG)msNX23Fh1fAx5Cc5QQ0`NMtT;lTXP! z6i9bjr>oPSr|h{UyPMxn*>bby5=X4~upVAEI$_JHok(l6F!o)&E;Tne_7^byGWqFY zZe|4^GqKFiB@ab7!{S$+HyyE-nF(oOu27}5!POrk9@aiJ?=%x4@Ks8A698AZPm7LbgmC+ ztg*;mltuH1wt;}7`N9q%UI_a(@9PI2jb>}q^e8TBCV6=& zIb)33tGfu~m@XwqHpxbp9@VCozvXYj!unE~zPi`r-PA-d5!@uJiS}5mtZ(RcHS4Hv zPa`PCOj0o%g9J=IyG5y`N*zO7^wc>|e6-^zS;R*tP_r=h5NO!R88FEFeBM9v)@*ow z@TlugeCugSIMcqAl6%_Y&n7D$*1iwH{CYoXNEvq8y`ZbUc`#XH6)KS6PV=|!kjo4; znxu&Tmj$AP><QjOgg^zN*XYMHo+`G1`or=VCSyU;#G0P3KCBz9t zC&hLU0zNPMA{)-)n|`asg3)xl?(F&b=AHe@PqO=Ue++7G9(ue?b+JU2bW3FHP^qBg z=dXUyx*-I4@Gc1M|IuSy#-iAaG8{FCCH(saQ&MWz6l_b;N|(D-m*%vGjy{}MDpu*< ziccs4h=Yyo_*<;xjK$C#33*JmJ#(O`wAoUev+W=-Q$m=tzsC2@7!wzzi0K>D z0=j*l@2cH-v;>GHQ}F|ORy_kqIBc(=mU8aDvA~g#+4Qr;b z+lKe!!~OO<*z)pn)~{xA30cllHo7#9%;VXyP=XFSWp9HaA?OaK4bMuO9W)Rb;be0s z6cNO~@XG`;8VzcxD$JCqgojUlLE+Ob2{$dP zk7xe#m?~R?jbYdk#4U} zqK64QJD%suz=UJ3G?7iWZo_i*XAw;IOj#kLv+u+!M&#|FM$#pdu(-FAM?dm|ElUIR+c<#=gPIh8to^NH}^)#jY=oUjV7YNq{(+ZUBk)?ATg>t-_BhP ztgwZhu`Z#@cOL%$Nv5Wb2dIZAGGtqj3EH(4e`fAC@`6?a!XuzpSDXRzX@diff~0|= ze9ENQaEbnD>$(1#Dke}#tyM^QD~Gp$;Y?~z7N$nM>1-LQrA~66D2(Ef{PD6^Q{*wp zYtQy1t@cr)(RgN0uD+?wlT1msIGe_WZX>jrL*amAZc$c9y4VSb!qPnY%L|Rj01F7J6(kL4E74&Yn4<#xzm{COC zr1tA0X3|E&fb{fYqIB@Oys^!_Czp_0{j>Q!wVI$5OVvQj8Z(ykhMlXM$K}YEh(=2#;gLD8N$p8pEF084%PXhbaMu1CnY>(H5#Bq< z!Pe?=56Y@Yo_7f11VJ6{?_WS(A96WAl=rJD@L7o4_AlPBV!js#nxoN!?iFknHG_ul&kZBMND`n1en=9U@n_{>Q%WvR6~pKDm=U zqb%M5MZqrz8D4jfE*=}nySaKJOMl|+P2~`x(ff*~cSR?FrJNIO=iu$EF?$8FGOuNU`zh{qix6>^t-Rc zRlQ_4kB5+{d(ZzUmDW`MpOi`krkW*1*-UDsVA7jT$IeZ|$5ig!eW6D6h)FDt^63?) z!|is*t2jXs1?Ucik2ZJJLE@xfizjHugy{p@x=S6;z43I)3GRTeN(2I?PpXJrP$Q+N z6|8BsO{3`?qFHPn58t^ZxtPEf{s!i@=VlBR4`?Ht=|mgwW2mz1YZV@J%g~7^uciX+ z6;ZW}{gjz@rf?TLu8EU>`0`;LIN7V;fDh;#-QLLY+EdhGP=-m)sCQDT@G8pAVU|o5 zdUx8+fe_|G{>}5(8^T<<5G2i-Uf1Q$`60^b{(KSM(o}@jp_$C)#9?;3E8;edZLn1t6d8L-Kc z%Kqc}+XwjMZia?XjTxEpCyV3v=mNIX)g42wO)JF)*EXMaArpSpiszCJVX7D&-(P+Z*de*cXtv+Uq(;&7&Eo^<#obO=#L_D_EX$QcaBu$l=rFX| z`(|T(58|F;danEowPHOl+QP)DMIcH(#@)4XWm03LI3M*yz5~x*c*~-rd70!9&KeeQ zR}vJgnVSqX!MG|j$vI0DIbJTgm~ZGUcTup~U0s*nr_22GY>^6>F*SXla(099QkT#b7+n%;iZdr&k&};tO@{mM<{=1}f`SVv zu>8|YuP{+3E>UdRgf6@LjzM8L&%-*pp51r8Hk+aD&RPvvsYvW+c*33r$et?(in3cO1_OSw-Dp$No@7%zhADcj1D9c(zIkv))`_I~>(5e07Gbv)L32!_o&) z61jeevYyp^hz0%tO`FRtXnaO~LKiFD>c8s2nW|x2_j>z1%3pN9#B8ic`N-^wGE=Uh z_2cYeez^Z-*Y#}4)7IZ}F}h-mW>#c#87WUYA;+$LRRfq&hTL6;G7UQlf1M#8kz zUX(ef+D*1he%fvZ;z}{t;f>HT-oFG{>ur$u?{|$IO|APXS0oW#KJ) zt{rvaCaia6WhNXe4Sl`&$%G_(-QS&$ zfC)VVL`MLnBgDo#9dUL=>bOeC1+ZL;WsAnxNyELU98Sk7zaXn}4arJ674u26FzD)_ zeG2Z|3wYCGe77)PRol=ZiWrE3*`9hM*J%CgV0-B-xPCh-zI&RFsbdQ4Bu+Ml-HRljyJU~qeFS8)s-B~#z(8?DwO z@SG_@?REln9qmDAc-hc=9;#5C)JJ^dkSf}bi18hlWP>E0R*sKhQBgcVh!5|NFk?1{ zX@LvghMEfA9zPnR|Jv||!OT(yYSweWA)m#^obATM=etFQ^ zKaOd|6c)N!{9)pJFyF1ROijK`YFSIf_5>y{=D8EJm^A7;$s@CPb@bM_mt%Lff}!d6 zL9mQI(@m))8kxTG#w5Xdl+bVICe=UrI1-_Pr|%?n@aK$;L{rL$-K6SEX}88FS^A75 z^&L*nB!}xX#o1l8cBit5*b((4l`W0yk+gVsCz_0-^GlbsI)?2jGTY<=7$+!JKc%Ku zHH^JJv<<~SUYw`=O>_-{`@eTX*=gbCXR|73e_{beDwYiGh6j5 ziUVo3D@*$wyBc$x>_^}^jlqO)Ak!e-5^vnQ{;l~nTQc8%Kr2L^v68($lZJB~j>oHe zD)DIyXXwG+e1ym~bywuf;%~9K>t|iYHVpdv%4ZW>5a?;fGh1b>%#}uZ3j31jYE1l5 zqOvg?DPR%L}gng3LM zC$8BUXs43G^_*lBXFvRs+f9HYv^1M)`ZGO9US6#Y{{xSBcU9S9!PD7pLQ_+tPTA~ia(BPK ziO!U*`gx3ksa~hr7B9ZZ1(5{s*0Ob=pLK$eZKyuvAUHToN`T*|(({}x+FYE%f<;bG zF{GQske!HY+cyUcvyWWfS+sGxJvP!U){QNYfj&|AL!8rl(vrXJ5)$Sn8e%;LUDR?K z%t%aGdt!u)>U2f;ex}XKlN{}TS-anqeZuf!tWEqf#ybGTap-JI@lkSCrrvJ9Q(pMO zEv!DyPIl+|EjMR?Tt?IGQkhT_w}T%gtoP5a-p(ru#YIvOt9Ep72*&zhEwbw+J@#I; zp@9{ctZc!hv%h0mzj#;=L|T5h;%nhYlr_BYiadlDBb*)Abyv`ydPEo24XrO~XmWY6 z@Y`0(Bng|8Pgx(>Cu;p;p(+mlr%Ry}9q6(CfAI4Hb8Um38C|ztf;wI0Dxj6&{{OclsXi5xncboOeT^Jb^9m$xTLGil|*Hl(e0Y$e(HBIrwv5A37DNJz$E!AJ`?JU9R9{R43&08I(76>NRdlu-A36*MGS2@}T@8`?2Yz^1S*N5qa@(u7u+C%@ovy<&gWEs*)nz=DHIgHy^Wdae^`8 zO%DA8_rsP*3Bdlnh2Fk2>y_%dzK#A6v8|v%k16DaG;zGFr6-K#s!N`!C-l1+yt`p{ zQF3s)g+QG|o_}+{lkkW^kc`U8R=VPIyVdeq-h3CaIBzC?`n>uJ&=Ku>LH;Mn&C}e4 zZ*9F*3%`y!0%yA_bgfOX?pWX;hD~c7fL59K(&lX{X#C zk0~OO;b}5HRvjxi^>)NV!0O*FqHOk`<(?7t`n$h%fChaZ7{647zkhRg^vBWMPwS#H zkv#XKTn3U5N{j{*AI&W+;ODNwU(z`v5ndpX8U`@W$fGD>u`9o|`TH}fK%&1QlTJZ? z>dNckm-knVPcuF3lw(P(bCo9UGRPuXkywBRL|CG<{_Qpvef*;v zE7uPsC;tU}%PNx8ai>~!mPG5Do46InsX%G#7go}rX1i?-m_Pl2)>QcI5hMewKrwbG zc6BCQ>w!!@_uE!tbrjz!2Fq?iPu;negr1~&G}i=*UuE(btXLM*B20k8B2QP;({_=k z9npI{&u;^_x-rTRAWygCKATK9R~Wl?cOJ07AJsa&JOn?)h7>7tf?553a*NLMSZmz- zW#~rnd7qS7<>#a)NuTn2;Z{tx`RT&fcekpa72e_4f2tVI1`!ioP|3SSd+516!g=c~ zQ|C&{a|@&EjQ{khfBdJ5DLb7r)prjBx`6d+&8LR||N4^h7x6rED1UUl7yL>KNG?2A zlLGQ0%35vLB{2QOJ}(e#(SMl&!fp%LG^@Y04aEadL}e;2B7twV&Ja_TB~=D@1+C>X zVf&!X?j2}|O)V$xw$%eF`Ku1S>B{R%3i%cwaUQlD|czpc2lS;ejRiNat-Gk9%G=Dw0#Z+pgZ#BZiWxm5=wA%4Gj}C6ee{&mI{Hd_sP`%G)Gj$fx4@F z8t>&-K!QrO*&9VN)7IA38{S7nMRg0r@l>n;;Y)9L5~Y~20J_ELscqU_`7=cnCXklA zPGdsC>-?(ned`Qib*m$8yDywO6(YNRlcX?1zhzvMVwfuq+g9KocJ+8I*VJHOHF(hJ z2=<%_=C+>eW2nEm-l$2mn6YVvJK5fOp@;3t*D;YkqzGY@k?8h>s@<=xQ@o0S$HX#o zOHI;Sk4_nuHzPs*;x19{iT(YknrDrxQAyr#DMHKLk$5)=s3D$%;n`SRe8)?MP|#Xq-8s=RkV(exgE0)eFe-%XKTr50J1+NU#^c7vfTQf8D;-jME`J2D&1p}!8 z{v>0~#~mr)D>!ttcAzGK+IiqXtk6gvs+?Qtw&!x5%{Z*>comx9SYBtUSJ>zn$Mx@W z__4f{!Vq;>V#NRMCH4XiC^7wMzD*cdCC>r}) zaAV4(ic$SpKmI9@Y^X#bn#FMMWp`4cW1-omajjizG;UL|pWnN(ZE|8x1(Hv5as6gG z&&IY{mUsXjge&eoWw#Oi_-_p5=l^e=L>(=Op?K71MqCHd!U=qrJcIcLJE-s4+SkFH z?TF~;88EQ0_yV_%tyX_DCYZ(`>pUDJ1qMHsiBX~?MTLJWvvGj^V2qPhT~K}m_RBAx zB62?(&jJM1=puV<;E`4H6Y28bEOxT=QFbu&YH}TiESRCE?Txubi`>M7tSpAl#l%8K z2b1h*yD;4s%U>2|i^AKN+F^!Sb@IGJ$rA+?VO0vhn+_QMnalqSI5J_^Q!3u?j)tt2 zaj)a!*%g_yfG~W`3P4@qVz&cw-2I#;9)PrLx!hEn#C$-VALEok3FH#NIUf!Hug?#* znZ<1R5d?5Tcms1^h|}x#FSN0+hFwqiGIzi0PU9aq}dWvR4{ z9^H|flZu?{n$QCk5o`Dqhc6DzVi&EwgZC#IlWPM(q5+p-Xf?Jx++G%IAaY8t1dwtu z^8*nQiLVdmK289so^;qBqAB?xw)MY7+|Il2fu(QQ=(xGL8KksT_rKmD0(g>(B;r!5 z$^N-lnx9c#Zr$FOBvmX3wBgEA>ZyR$seVcqQz`6uv-;;PsnL-v(5lz*QsdyaZDkc1}zXaM2qvdGGLWaRBDy>ttoJbS7S{CBN+s*=0*PMa!=w@Qzdfg^AO60Vt8>9U!% zA;5bVsTAHGF4o&L47G`R-KH!5tH^jd7Eo`oQ;tY1Z9#ohT$q3#gpajmFuk?=c!O#W zZgEwz0b;`p_kq}-5C6BY>0HJ2UtRM*ofZmHx$=n?z?=QUHN(Mv8TDVv>WxZ;yw_g~ zb9#u+K`FBJHT@5_*G}5Al8?sQ=Wv%1k#3%>?spa9%YVA;D0?LPiubcAfo!IHfGm+J z0|aplyiZO`lL6#&fOdq@dbMp?5XibU_MWV>Srz4l8xjx@JOb?QWu~-0!1;cEpD=Q~ zJJZg1Pz4FW#8?jR7s?GLr}rAiHTECsODAmRTalKvAE+-gG!LY&{q*MMR>V`!2PJdA zz9_~`#bWT}@!^jQTefQJOf_*qfot>YKv%NA%##1M>wb#0h&t=R%XGC}7ptDG-UnA&?uD z&t+8{IO*J4gamiPdxBb9gCnX)%{|SH%kTL}WNBZIHbzV}bO~BK3mBxO*Xt2wDvuA- z-c^+S4!RCp>o0P=@jBrv5-9l5TQzvU!18J81?HWUxV0aekeX3;L!WZ`_t4>z(Y3zE zmh-zPCUm?r8wGJ715fjS$Gk)OY1ArTz5?L3yHiCfk5~~ssX@bqMMX^HZ@;!zRTl#Q zUfI33E)`K~YK3+p5~+rsG1lc25=#^9lfAx~(fJJ$(>0D3zJ?^X?WzfV&jWQyyLeDHJX8dRAQ*KL6Ns)3oUb;Ddit@qb1=m|IvjY_4?(bioaz$72^YT#K zT3S9ZGT;ps$&1`1dg`#fx-NEFzeg(Xd9ge#8X6Di;d9jOE&$S3ctK- z+7?%Qf9;sn+@H&);6CzY?ZuAI{T-`ZgeErItEtYXh4O&$KShNk+VF0IL9-0atr`mJ zqR2h70&AagJDp9tx{#)OmAr>D^i7UWNF4C;jF0uaY$7J@l%TfT)1qAI^>O7TyXBE# zVIaF15o|l4Z=nVSo+4geF8-FIaV)c8Y-Z?LKjM2+U>3c8udB}--tp&%qg}y!v|@D{ zV00$yJ8_q>e~*E99+Dp;Z&?y))&D3>+$O4RX~^LExbV0`HMDTWyy_K8vbg1@KX@WFw<^@KOGbtn4^T+4Ld9IcHJ&!qys zk?G?(V-CcESMwHaa@N{ai^GlUDr?6O72~64#ZT-mx8>f`ZXVhHY`DOmmnp|SiC7w4 z=8z79Wev6cK)xWAV|(%96ZjN$8`J4!Okdv|>hRfM=GV<%)h|z$H`sHhR@2e`R~Z1f zd!Q7B0&r=V0iahEnk#$i1;vIN?~Y|7hojlg3mbg_7I4*<0Dc??V=k$62E20s+N^P*ksgBJg3CX2r`JsC9t0{>KB%gI~)z?ArrKG50yb z+2Mf<-7yFBU!6I$8SQNq7G5tty1e1yz1d2ht@CEPYp&*4Xdv9&yQ1u#q?j}&?}=(Z z%(z8+dkm+#_KIf-oX0!NKB`}bL+IAB2Ww0-|HUobGeEU`2_;U&vQUahl^B?e&g=*qFSNW#GJ3_ZctG^6) zUGIs!ZT#Wa;bacYz}8js@;By9jiqm*`nBV}r=M0 z`enwvyY}nms?7)m9ampL zUSjNEEdJUeyVw(+Zu=;slQSZNW8k=W6%u9`;LTjBBspW?|b%528?x zM;PTnA4=SH*IBx-&9lFGOmi(g$n+!AoIf+zUOtzPOyPP~e-dnPQj=$JL}3)O_tVVl zZ4E@kwR(1^%whB!oCWQTd9DY@h1=y_Hw=O)K&Ie)e^)s0HTi3oO9j%J4Opl;r))e97x4pu)&J}Qdz4jSXj5f?y88Po1`7Vo zV?8R!G5$vi3Iy2mQ>`V8>DtKg`h?fy|m&$SduEaXk)nj z26^t_La8R7gdJQK^XAcZ=?o>PFJZR914PK-k8`m%_@(il#z%D3F)pN`lw;)*bku#wpA7HUeu+pEXqx4}4C`SZ zY1!JzJe?KhxCAU+m?M#>QGO@a)gMxrnsDRb|7lYDIXoG`T?5 zfy5vRnXkv#DcBxOpmBM?#awJD_;=6R!xA`Qi-rR1-LQ3$?tmZC)C-pj&mkK4Xy<3s z)o0gE8cwG#7n0xRAY6$yYe(Ma9&&DFI|wnj4UYev^&@=fv7m^eTl1A!qDw0?H;Yuhxy4depogACJWc|a|3 zi7nDbNlXu`>EVFyvEBoAyb~1_WeAie_0H=~fs{e%r{wP{)A5|3cM--z$yHkaW)rGt z{5PCbKTRa_@k8SnLHDgTnCh&e*ckFc$c|;X!y_VnMHdfa#L0@WC5B<;!>_ko=;GEJ zHgB?jnEy@!kq63+Hxg6m3N`xHGkq?KP+Jyu)F$8fb08<*J!0s+3!c?|Xh?PH<)z}5 zd>kS2DzM++FJ0XJVMU^BIe+YTBUvmNf$VswOtJt0$rM|rs9C$G40ap9#($R5VS|zN zSEpr%j;!Kx6R*|%c11if6Yu8mytK8%1NuU|*tBm>_hkDeXFo&Uls>|lXDBdl$6|`B z;WC4ytqaE8{ppNo&5E8VgVZ9eoZpt%39BxSCRM0D8-VdQd*+)pyl!hfv+@hQbV7!j zVE{mpe3^0$b-Y4Gp7iQ0KIUY;Hc>cvlXhUvhm~^Fp0(d=6=y@9ZhJI;gMJXnCV zj#Vf))2dAjmMWNh3D-Dv3OqmSo5|>0=|e>unr5C|l)lVt6O~yQu~JMLqp= zc2qM!({y=cyieVU{XQgN#fD-@b_!2doAX|vi1DJ1yXD2r?L8W)k~i9B`}|>rpi#~4 zV$I4elm(pg69#6-@g`>>L1Sm(WPP>D+M1>$hq~HK0dHY`k?*hL;sRr^{H(+VTyJ|b zGczqtPEPv&k}{=KKmT{7aYOsIW`C=RGM`9|O$Brf)U+i}6*HOFIR%v*<7qfK1));N z`VfRq#>Afvw71Bimdbw?0W7cWod^#c1l!Hdaxz?m1I7_ws0X}msfGKXmGuiDtkyRl0}@hebk_8S7t&U^jXLm+8+tx(d#pwjs zZeNgqV*Qh#OZeX;yPctw8l5jvfS0S7kcg;9KtQ1N+Y=`SsD>5mcLg7fl5Hy21A))+ zARqYc$)>+l&N_;yGjhsjwhOq zGL5Z}2sN>Z~ngeyCeA6K*&-zz(6Ko1FCx?0iCU)Py z|6o*)T6FHMbvge|y6L=42&Wp+R}KHxtuEKbz*lQz`|T&i_GyMVfdaagnBIk+koA>) zp;yj${uMUSP0pUzH*!B#Kz9wem3l9SG_87@&KJ7S&>3pVwVy=kh77jzT~)R1sz_x#^%YjWT=!<#ZsL3ZGp$(L50A#4lNQ(G-dKXk&%5+WQjdHnvD=kcyrCGz|01tfRV%@HjJ zb(yTT*JIU*RIjY8syx-Wem`y(OaN?5+LMThHCR8zBFqbAE30rH+WE)1de)bIo8vet zq19!>=_9ZOn!RAQ*)=eEZ%WOi`Z?vhdlLC4kuqe_xpBp zzUA;2$cJ~}kdaj?_>^+a@!xj)TPb=FN87bQYmNuvz8X-*LhIeRsO68IOnO~P1y(b* zFf4jOu6Rln$IS?TrpC6q2WvUGMz>kTA~ozDoUu7hWG56eT2FcYc^ z_$zdyv7a*kuRihFK=YimA`Z9L==Z`gkq1c)BihTetka z?Jv&mPMNT@8nR_X7^bcV(9*np)7Vy$@ zW#UAe^`l;Ou(9UA{G0L?;u6Zz>+$(!Rv2@8ruWyLX>h~Z>?wv^X9h{z`Fh2r zi`{uIpDBw6+f1n$f}f>cg25;{?v!&2?Fsr9SIkYiog9lO$g^&v)p` ztGF{~`yPgwFoB)0ISpKl#)yq(o}j~j+wCdGl=mA2AHcT#fxGd9WZF+?{-3L?1^q~i zZB?Y23We84<74jaVao~>-Eu8`njQ6^vCb5VtbO;<%YL3DJ4XV4O}X;mCx@VqE`hM0 zsP}!XkhG5nR>v7>Q`B*I2D-Md6N9_;z0xc2m1S?!}q;_J<{(ACc~C2l~VH@?0Fhe*{D7h5yWr>R>B?(WFA+ zu1d2I?@k>kT!)1N+FcQPM-Bt5uLPQWWv;yxNnyvK;!aN4R#qW33=D>SZH2DHgb`DY z?GrPv?vZ$;Nzx)I`Th)2xUAaJErP0V>W?oal59wP`*1e;q*~YE!vdOCo{<^T=xl7< z?e3o%oMv#Ss;xj5-ssjpYwrZ2lV@;7S2Fq{7|DC(56~D87PFe@<_Zy6Zs$H!9kg?O zvYbbnPUY!lsNQMsoVl0DB)BH7nEpjC%|HS`H!=7)grzM-1o-kVFd>kLMrYk>UGd`Wg3P^qi47kN0~fp-+gYAYWz^$x!$dL zveAn9L%4ug?W)1fs84dzI z7mP(M^{$2)V_VDZ;55EvdrO+7K>{!bEOr&*evRPSX6h?Q|J*c1^XnEkcN2=U8o1FH z)uF*tm~vMoHV~nLmse?6=t>3>g}=;1*Ut6J#((uKW|BtI_Eg%@@4!;%XRRfpWr?!q z$|8<<)t+wwxmxM&3kx=KekJPd2UpavQn#`SK77mbuG^wo{mDuMHP5{OREsAx=aiYx zC*5~wt9Kkb*QX{NmHW1)Rj`0}ecU3{C>M`soj%~%0J(!+SKA6KWtHwVGgC3`WKIab zC{o1iN(eu7;~aqId|FFFudX^dguwdKCJJk)mz9nmTy)6XU!6_FPiw>*ga2~6nz7CUkabO2C=2tK<5Hr0HxC##~*>_YOS6^DAz)T#q?wMsrxw^ zi<*v@V+KB90uk%}{x?slC^x)Fz1A81-9mZJN(*^zTTUQ;jQ3?MwJ0wU#|s+U%WzEI z6L0_7Gl9jA%*{TOA3OP=d>bPTyw`^bf~IShudLc|vw_;_Rkva3(zU*~-8aJE$Q2eJ z@(dDf2`=52G1@EFd$Z`W&K4=tXiDmQCM?i+=9wzF=CxGr0s$n{07>vM{^+ z&4$Ms3b5XzhBde!GvTu~2fKWn`gO=D(z5?{ecP4%0-DM?#45tlXc=lqB!C_nF$3u~e&xNTtKv>Q}Dv%Iz| zvFsOm=w6U~tCt4ruD!gd?+?D4rIVNMoq>!DglsfaKcb}$1TS5&t+J4ugL@A)zYq=v zKWA>ggu;M$jv)9;gS*}K798@dptIwavVh5igi4bOo|~VnMlehUg`T#kq)kbs6<5Ts zhy9-Hr+Dso^o8ZyM7ndL>?6X6WX+haZ-E_l!(dnyEL1&H0L}dfZ)@$(eKpmina)R2 zB**Krv90+ShI+*%@rnc&wDdxLc!<(*@<%)P-xoKZn=bIbRM?+H#J4<#JFwE7#qCab zekhvKm@o?WUREOoHpvGwWodQa2I6nt$pNPCI)x{+VMuGi)zKo~((39e9b^wsK%fFQ z-|ouuc=G(eI(%H83=EhQ|IL8a;`02C&GxtoBT z+`8)tpHP(gyS7$ElK>rnHVwcSWUgNL1G)KW)#VYYqG0cvt<_wam=!ZqQ;qOhdAhv2 zu!sn*g=(Wglc&erRnLpzEQ##T-M2Ilrvo*T9%0GgR|30$Rf7;0ceCC5lS-t`cur4i zG?|dz4_R7&fV=EjLim(*Rmo}Cd-xURayZ9H&&tYrP(AX5Y($%RcL7xN$(~405`ca8 zbRAAgO6spsPmhdAPMSlw3VadT0I;%kplSN%ViW{XiZt=cjv?eUz-%79t%(i|tu%nS z36uAYtbB!W=(WsU0uCX0+`&j<9`mG(jAaWjcy&Q0hMepdAwY709{?YuDIq<*bg*d~ z_)P>0s(<&I0A$c;Zcfh7oR_@3JZJMydf+#Ed!28FH5;S=|7TTGSLb~I7_nj9VL11= zP|b)bQ@_h0_helcqYY_6pJ`GIP;Cc zUv=LwdE0^j#F>wc&52;R5xrt&i(hS~5drp-089G(CzK3!+SUkA1Fd){v-rL zyuC)BI@1O)Kt>@#18{lLmigYj3pZS5u4KpihQz8}XIXAQbmfwG{|pY?fsKI}ljBmt zRX}io<8&}{=?=dty98(yy-dAN-S@rm+>jQ5!9PD(OaSi4%}rKm&j~Qi7h4?&H z0`DQfoR|WB`7l{g3XX84i_Nd;=vvK-UHtT;8pMp#P!BizAW$CJv|Q(_*#66 z%^dszfE*MtqB1C*5I~TX~_>idx~-Fea_^plz0f$Y{*5) z)41-G;O^nZ->d{Uk+H0dDv>5WU7E3jojHdGw|j+I_%&kTh{N-JHAl?AVf4KKoq?nZ zZn+#T7Jlh4EFsnN&QgRm*8M4B1UO*z@d5UtB<22DhL`7PrpPF83_dBdzXJX%=I2EK z_t5hkcuG@HV(s!a{pN>@UQ}HX!8QuKhTn){^tqH2t3LOJa{P=jeS;iQX1SolE z#sY=T<;3~fp1MJQ9sG0?icx=lsn|h1p{Lx>Ls9fg4d4{j@W+fWpKFTs126G42CHB0 zPfG%)E-`ZmEd(ABE2#T*E5CMdu!;eYn3izLq}EK}oEBB8hJdNXR|V~PJ_ay|s1D-k zSW&)-v!iR)*aRFoNX5c|5TM6Iw+^bx`(hM(P1%u|LXCpb6+YJf;rllI3MTx`+w1EF zWAN+~4+!tFO@H0O`-H?fy*g`IRqKRAM5Hy(e}&I7E9tLS*OkoGsW+h3DG__(VoivZ zj9iY5Q0GET6qtKQwq&X~i%2 z@FF&JJQxI!mhsGMZ!-770Avg;TsX&d=;>G9>WsFr&r}1i9AyKB6hKYRLi{|hjnCZc zb3VWzLrO*lW+Ja?k65mr$px-m=UeANEp+3jq0!?tv;WQ`LZ`CIG< zmriJVP-!3-@!$)*u-i;*qDyc`gx`yW&lXgA#dzshF4J5Xd9Npd{lrM<0=qHvxKxY^ zi4H3gtHO|j5me6N^I{b-8nkxySeHJx`}K!^A7fG$r=ects*Qb!-wjCtomO66F3{g* zqi(Ue2i^&ux4l;j76)-V4izad06rTtcvk2%Pm$i90*bF9s-4-DNjJB4^|&7wH{7N5 z)oQukHKHBAjhlg)W>0UAR@+2+kg_M(YfGIF0J26o#LEo$JXs_S5LHj%%(ky!oK()3 zwUti3;zddH(^gEXj0&s`G-ROFC=up}2%rvqLdlEmUU?~Ya7_QH--a*8R6h|PX{rjr z`UQPrzl~05onKv+DNPc-hi>Rp+-&7%hIXe)^zuxrH=3SXJ(|I^njyx$njuxBm#$OK zYBR0A$Cq4(n=t5LmFZL)G8oQAdGJ|CX^nEcxbf`K+}m==8U-*FK?9M4<5DAQq4^Ir z6vMJ(eyp5RYu`{Bd#kaN;jBc)s@`%=;n;YuYYZrGY!Hq7T!WS&D>)@w%)%OL->Xyj z@UADJ_Y%dzN_XZqKl2h5dQ_kyEVx|mG;>Obj>M_w1C}!-B)MmRai@|8rFpzz0(e@^--6>BY~R{J*9(fA7se zH}F6Y``z?20p#k-CbRNLGHc4}ff%3l*4y``UR_5<$It1*s9nFN1a+6&x zBn9G9kKPjvy%ViyC|sWMH`G0AuV(2^j)p3iRUc=}0Ege;-l*yT*pUEuz?(3gU@IsJ zOI>dWY5)b!P5cc^)_f;Rove-T-d-B5@S@F#@si(rqnoQ)BOUWj5kp5QLEmJwl!qSpp03K@+`lcwwpWcO;#B_+HSV@hIZWRB~=8|-SQe-!N zVP4)|9&#o$9v&P#i2j?%Mi(yTf?AVC67ic4GJbe}KaUChsyBEW=|K9Rc`f!6RJ#jH zp)(LAqsrRgFMdKaj!~y+ViPLfX%z9783ioT{Lzil1iYH+*P0Q4wpC$5xi90HEb`yp z@r;e_dPO1HThjbBQW%Gq++Uf7-~EiM7b;&8TnsgP`jyOX(PoTukps|Y;PJ#cx+(BS zTt399v40V9ig^nE4TC3KiS9BybvOy__}H0h3wc}T0X(OKV+AmPKtFn?11G%m=zpc) zRLixwEgtT!r9W+rfQRJ$Evc~N!*(;m-!_zkPHR2w9eeYt>M3wqV+D z0&aU3aK5(T1t#%D(72N3`ON|J)QtWuMo~!o)2CcFQBnUm8Gq2)xy7N-({*2OWp%a3 zP#}QKhRCuk`kc*fR~3e!3s;rso7@$vP0vxkNCvv>7GROJqQxB2+=|yX`#S?RzX6co z)=&!dn{onPli}1&far`R6xvrUnA8yoIJ@w+aY}gizGrZdz8F9|b^l%)WxxO5wRj6M zugR9EHQP%L>NGP+-wd1}lEUr@uHkTs9WU2mz24;X0^Jz%=bx(gAvl`bVHtB7nzdg| zp1!bz6M8{acyK4P$R71AiOX({ileX@RaklduSV-rNqr&4&c7H%x{`6Au~#l(lAwgr zi~1)Ah}IR+UhzqCe@<9t%gTRON`ntKWG z^+qlQ{|;(sSI5C1*}1hi7Ac2%%6C*&q5rIGdV1!_?#k+xpQ19oy7<8@4m3|_Hb@p# z=(gfOeak!kW9OD75(IAp9FkiGr#ikM5Gz9Y3*bIR4~W2f1)&nUe_kvGokq(u^-_&! z&EAv99-)>1Aq$>*0*k5-o(Re76#a!_fBpwhiU6n?JyaO(KW6kle!PQ#iFsi)T~5eO zOA@CEzOS4C#6Cn|_XhWOiZ5Z{k-2)HBUmyISb{iE*79*A$1F-A^xB;~;MlJ>oK+P@ zfM5#)D%S(nqL=0%!r}khq+t9gBcFUt2xyqNCIUlP3k<>k^Y#~d)d4Q8x{Crlbx#qUn zUuQR=q&Xr~va@5d^Qr)42&fk-EG2*q_i}qI#|40XdrCl)y$78Fd{57Si(+hooo?-*o2CbX@!VphiLm+tE-oyxb_(7Z~)9}$h{R38e08d3BjoM zU)E0^YUuDK!fk;%h(V?*mkKVDol_a*4r7k+d(;!N;)BM}jV?-Uro-)7RHOI9zIVke8PJEmU4%W3!b1`bVv9(livu9#z%(a0)r?kjf)K z<6|5V_5G{^BRf_P;1^LR?~6#L`}*Dt15+$YXo3-3y1oaeQ42e8zKmd1D|-B(sPR3% zfxc^qXoFT0QpF7XP*tc&NjsvP>Z6+R1G_{qY8}b_W2FK8O^SnqWA*QpQye>n^9-rX zLcRU?V`Llo66RvGOB6>_v#3hKY=s6BzgrboX?;;aa6O55-C6MI#VoNG?7FVtF}Z}e#ZiJw^PIbbp@sXQv$XtKSrU!P+>8! zj#bD5AZ%h6M@P>|kez{mYr3d$vS>-RZUdHG9L`<;J`kIS176_ySD;OI05A`5dTHkv z!je*>E@;C2Pa>VZ|M2yGP6c3kzEBYsWg6&t4v0PykF7R6I+__4`*&vs^@5Z5XXgbWq|S#AQD0=Dpiw>8c_qzOxP~R^u~Ve-6tlop zla-Z~1DI0V+uOK(J=FsrOn9TeMXi-g=L_7hv&SsAJ59lGdd9qWy8k~h(~3GiMmam{ zU@ypZ3=mvEsDOB%+@kJNch3GQ9zCWg2yMvd*BaU^BRa7IyAm~szxDf~ND)A;Am?8% zN@$b>C|fQ`gA3daTl8vR@rs296Dln}-k7X;QW3WCneRK(PoF5ZPOp=Jz2?q=;B;RW zPaEz^Y#fDP>jSb0QEy>&u6>S_iHoDf$5=yx-ly4Y7-T;Uj?T>9vl?CZe?`Gfk8FWRoCiwPZr%2uxiv;>!oZ_9`3 zUhh^{z#4~b{X@k10B1K)7qR($cxG*LlN<%ri9yph2@G^m;R4KDDYG~6$VEX#$2#Ni;9Xilr)MW6U<{Q`)tLY zb2UU`4~Gk)nUM}CW6^(4|(&&0TNjrB<nC06n>ip2DuR5tOVXZ_N){LqL1kW39yAq?V|Pj@);LSqLrKaJ65zlK0Zd#EVKjzuyFJJNu35Jy_8XVbs zJViIQ@`$?*4tiZac|18-e%0dPbOa|XqrnB6zV!GJ(s}igKOtY|b(V#f$Sgk%s!jXT zc3gqiHI70EF4CsKN_L0�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 ZaZz%7J { This checks to see if the `Authentication.getPrincipal().getId()` is equal to the recipient of the `Message`. Note that this example assumes you have customized the principal to be an Object that has an id property. -By exposing the `SecurityEvaluationContextExtension` bean, all of the xref:servlet/authorization/method-security.adoc#authorization-expressions[Common Security Expressions] are available within the Query. +By exposing the `SecurityEvaluationContextExtension` bean, all of the xref:servlet/authorization/expression-based.adoc#common-expressions[Common Security Expressions] are available within the Query. diff --git a/docs/modules/ROOT/pages/migration-7/configuration.adoc b/docs/modules/ROOT/pages/migration-7/configuration.adoc deleted file mode 100644 index 42632bc37b..0000000000 --- a/docs/modules/ROOT/pages/migration-7/configuration.adoc +++ /dev/null @@ -1,125 +0,0 @@ -= Configuration Migrations - -The following steps relate to changes around how to configure `HttpSecurity`, `WebSecurity` and related components. - -== Use the Lambda DSL - -The Lambda DSL is present in Spring Security since version 5.2, and it allows HTTP security to be configured using lambdas. - -You may have seen this style of configuration in the Spring Security documentation or samples. -Let us take a look at how a lambda configuration of HTTP security compares to the previous configuration style. - -[source,java] -.Configuration using lambdas ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/blog/**").permitAll() - .anyRequest().authenticated() - ) - .formLogin(formLogin -> formLogin - .loginPage("/login") - .permitAll() - ) - .rememberMe(Customizer.withDefaults()); - - return http.build(); - } -} ----- - -[source,java] -.Equivalent configuration without using lambdas ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests() - .requestMatchers("/blog/**").permitAll() - .anyRequest().authenticated() - .and() - .formLogin() - .loginPage("/login") - .permitAll() - .and() - .rememberMe(); - - return http.build(); - } -} ----- - -The Lambda DSL is the preferred way to configure Spring Security, the prior configuration style will not be valid in Spring Security 7 where the usage of the Lambda DSL will be required. -This has been done mainly for a couple of reasons: - -- The previous way it was not clear what object was getting configured without knowing what the return type was. -The deeper the nesting the more confusing it became. -Even experienced users would think that their configuration was doing one thing when in fact, it was doing something else. - -- Consistency. -Many code bases switched between the two styles which caused inconsistencies that made understanding the configuration difficult and often led to misconfigurations. - -=== Lambda DSL Configuration Tips - -When comparing the two samples above, you will notice some key differences: - -- In the Lambda DSL there is no need to chain configuration options using the `.and()` method. -The `HttpSecurity` instance is automatically returned for further configuration after the call to the lambda method. - -- `Customizer.withDefaults()` enables a security feature using the defaults provided by Spring Security. -This is a shortcut for the lambda expression `it -> {}`. - -=== WebFlux Security - -You may also configure WebFlux security using lambdas in a similar manner. -Below is an example configuration using lambdas. - -[source,java] -.WebFlux configuration using lambdas ----- -@Configuration -@EnableWebFluxSecurity -public class SecurityConfig { - - @Bean - public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - http - .authorizeExchange(exchanges -> exchanges - .pathMatchers("/blog/**").permitAll() - .anyExchange().authenticated() - ) - .httpBasic(Customizer.withDefaults()) - .formLogin(formLogin -> formLogin - .loginPage("/login") - ); - - return http.build(); - } - -} ----- - -=== Goals of the Lambda DSL - -The Lambda DSL was created to accomplish to following goals: - -- Automatic indentation makes the configuration more readable. -- The is no need to chain configuration options using `.and()` -- The Spring Security DSL has a similar configuration style to other Spring DSLs such as Spring Integration and Spring Cloud Gateway. - -== Use `.with()` instead of `.apply()` for Custom DSLs - -In versions prior to 6.2, if you had a xref:servlet/configuration/java.adoc#jc-custom-dsls[custom DSL], you would apply it to the `HttpSecurity` using the `HttpSecurity#apply(...)` method. -However, starting from version 6.2, this method is deprecated and will be removed in 7.0 because it will no longer be possible to chain configurations using `.and()` once `.and()` is removed (see https://github.com/spring-projects/spring-security/issues/13067). -Instead, it is recommended to use the new `.with(...)` method. -For more information about how to use `.with(...)` please refer to the xref:servlet/configuration/java.adoc#jc-custom-dsls[Custom DSLs section]. diff --git a/docs/modules/ROOT/pages/migration-7/index.adoc b/docs/modules/ROOT/pages/migration-7/index.adoc deleted file mode 100644 index ac2ef5f5e0..0000000000 --- a/docs/modules/ROOT/pages/migration-7/index.adoc +++ /dev/null @@ -1,8 +0,0 @@ -[[preparing]] -= Preparing for 7.0 - -While Spring Security 7.0 does not have a release date yet, it is important to start preparing for it now. - -This preparation guide is designed to summarize the biggest changes in Spring Security 7.0 and provide steps to prepare for them. - -It is important to keep your application up to date with the latest Spring Security 6 and Spring Boot 3 releases. diff --git a/docs/modules/ROOT/pages/migration-7/ldap.adoc b/docs/modules/ROOT/pages/migration-7/ldap.adoc deleted file mode 100644 index 3bef91f9ac..0000000000 --- a/docs/modules/ROOT/pages/migration-7/ldap.adoc +++ /dev/null @@ -1,11 +0,0 @@ -= LDAP Migrations - -The following steps relate to changes around how to configure the LDAP components and how to use an embedded LDAP server. - -== Use `UnboundId` instead of `ApacheDS` - -ApacheDS has not had a GA release for a considerable period, and its classes in Spring Security were https://github.com/spring-projects/spring-security/pull/6376[deprecated in version 5.2]. -Consequently, support for ApacheDS will be discontinued in version 7.0. - -If you are currently using ApacheDS as an embedded LDAP server, we recommend migrating to https://ldap.com/unboundid-ldap-sdk-for-java/[UnboundId]. -You can find instructions in xref:servlet/authentication/passwords/ldap.adoc#servlet-authentication-ldap-embedded[this section] that describe how to set up an embedded UnboundId LDAP server. diff --git a/docs/modules/ROOT/pages/migration/authorization.adoc b/docs/modules/ROOT/pages/migration/authorization.adoc deleted file mode 100644 index 031c8a8bfb..0000000000 --- a/docs/modules/ROOT/pages/migration/authorization.adoc +++ /dev/null @@ -1,24 +0,0 @@ -= Authorization Changes - -The following sections relate to how to adapt to changes in the authorization support. - -== Method Security - -[[compile-with-parameters]] -=== Compile With `-parameters` - -Spring Framework 6.1 https://github.com/spring-projects/spring-framework/issues/29559[removes LocalVariableTableParameterNameDiscoverer]. -This affects how `@PreAuthorize` and other xref:servlet/authorization/method-security.adoc[method security] annotations will process parameter names. -If you are using method security annotations with parameter names, for example: - -[source,java] -.Method security annotation using `id` parameter name ----- -@PreAuthorize("@authz.checkPermission(#id, authentication)") -public void doSomething(Long id) { - // ... -} ----- - -You must compile with `-parameters` to ensure that the parameter names are available at runtime. -For more information about this, please visit the https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-6.x#core-container[Upgrading to Spring Framework 6.1 page]. diff --git a/docs/modules/ROOT/pages/migration/index.adoc b/docs/modules/ROOT/pages/migration/index.adoc index 3f7954e7d6..e52c9e2a38 100644 --- a/docs/modules/ROOT/pages/migration/index.adoc +++ b/docs/modules/ROOT/pages/migration/index.adoc @@ -1,23 +1,31 @@ [[migration]] -= Migrating to 6.2 += Migrating to 6.0 :spring-security-reference-base-url: https://docs.spring.io/spring-security/reference -This guide provides instructions for migrating from Spring Security 6.1 to Spring Security 6.2. +The Spring Security team has prepared the 5.8 release to simplify upgrading to Spring Security 6.0. +Use 5.8 and +ifdef::spring-security-version[] +{spring-security-reference-base-url}/5.8/migration/index.html[its preparation steps] +endif::[] +ifndef::spring-security-version[] +its preparation steps +endif::[] +to simplify updating to 6.0. -== Update to Spring Security 6.2 +After updating to 5.8, follow this guide to perform any remaining migration or cleanup steps. -When updating to a new minor version, it is important that you are already using the latest patch release of the previous minor version. -For example, if you are upgrading to Spring Security 6.2, you should already be using the latest patch release of Spring Security 6.1. -This makes it easier to identify any changes that may have been introduced in the new minor version. +And recall that if you run into trouble, the preparation guide includes opt-out steps to revert to 5.x behaviors. -Therefore, the first step is to ensure you are on the latest patch release of Spring Boot 3.1. -Next, you should ensure you are on the latest patch release of Spring Security 6.1. -Typically, the latest patch release of Spring Boot uses the latest patch release of Spring Security. +== Update to Spring Security 6.0 -With those two steps complete, you can now update to Spring Security 6.2. +The first step is to ensure you are the latest patch release of Spring Boot 3.0. +Next, you should ensure you are on the latest patch release of Spring Security 6.0. +For directions, on how to update to Spring Security 6.0 visit the xref:getting-spring-security.adoc[] section of the reference guide. -== Quick Reference +== Update Package Names -The following list provide a quick reference for the changes that are described in this guide. +Now that you are updated, you need to change your `javax` imports to `jakarta` imports. -- xref:migration/authorization.adoc#compile-with-parameters[You are using method parameter names in `@PreAuthorize`, `@PostAuthorize`, or any other method security annotations] +== Perform Application-Specific Steps + +Next, there are steps you need to perform based on whether it is a xref:migration/servlet/index.adoc[Servlet] or xref:migration/reactive.adoc[Reactive] application. diff --git a/docs/modules/ROOT/pages/migration/reactive.adoc b/docs/modules/ROOT/pages/migration/reactive.adoc new file mode 100644 index 0000000000..370e52262f --- /dev/null +++ b/docs/modules/ROOT/pages/migration/reactive.adoc @@ -0,0 +1,100 @@ += Reactive + +If you have already performed the xref:migration/index.adoc[initial migration steps] for your Reactive application, you're now ready to perform steps specific to Reactive applications. + +== Use `AuthorizationManager` for Method Security + +In 6.0, `@EnableReactiveMethodSecurity` defaults `useAuthorizationManager` to `true`. +So, to complete migration, {security-api-url}org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.html[`@EnableReactiveMethodSecurity`] remove the `useAuthorizationManager` attribute: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@EnableReactiveMethodSecurity(useAuthorizationManager = true) +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@EnableReactiveMethodSecurity(useAuthorizationManager = true) +---- +====== + +changes to: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@EnableReactiveMethodSecurity +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@EnableReactiveMethodSecurity +---- +====== + +== Propagate ``AuthenticationServiceException``s + +{security-api-url}org/springframework/security/web/server/authentication/AuthenticationWebFilter.html[`AuthenticationWebFilter`] propagates {security-api-url}org/springframework/security/authentication/AuthenticationServiceException.html[``AuthenticationServiceException``]s to the {security-api-url}org/springframework/security/web/server/ServerAuthenticationEntryPoint.html[`ServerAuthenticationEntryPoint`]. +Because ``AuthenticationServiceException``s represent a server-side error instead of a client-side error, in 6.0, this changes to propagate them to the container. + +So, if you opted into this behavior by setting `rethrowAuthenticationServiceException` too `true`, you can now remove it like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +AuthenticationFailureHandler bearerFailureHandler = new ServerAuthenticationEntryPointFailureHandler(bearerEntryPoint); +bearerFailureHandler.setRethrowAuthenticationServiceException(true); +AuthenticationFailureHandler basicFailureHandler = new ServerAuthenticationEntryPointFailureHandler(basicEntryPoint); +basicFailureHandler.setRethrowAuthenticationServiceException(true); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +val bearerFailureHandler: AuthenticationFailureHandler = ServerAuthenticationEntryPointFailureHandler(bearerEntryPoint) +bearerFailureHandler.setRethrowAuthenticationServiceException(true) +val basicFailureHandler: AuthenticationFailureHandler = ServerAuthenticationEntryPointFailureHandler(basicEntryPoint) +basicFailureHandler.setRethrowAuthenticationServiceException(true) +---- +====== + +changes to: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +AuthenticationFailureHandler bearerFailureHandler = new ServerAuthenticationEntryPointFailureHandler(bearerEntryPoint); +AuthenticationFailureHandler basicFailureHandler = new ServerAuthenticationEntryPointFailureHandler(basicEntryPoint); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +val bearerFailureHandler: AuthenticationFailureHandler = ServerAuthenticationEntryPointFailureHandler(bearerEntryPoint) +val basicFailureHandler: AuthenticationFailureHandler = ServerAuthenticationEntryPointFailureHandler(basicEntryPoint) +---- +====== + +[NOTE] +==== +If you configured the `ServerAuthenticationFailureHandler` only for the purpose of updating to 6.0, you can remove it completely. +==== diff --git a/docs/modules/ROOT/pages/migration/servlet/authentication.adoc b/docs/modules/ROOT/pages/migration/servlet/authentication.adoc new file mode 100644 index 0000000000..3db94cf7a6 --- /dev/null +++ b/docs/modules/ROOT/pages/migration/servlet/authentication.adoc @@ -0,0 +1,187 @@ += Authentication Migrations + +The following steps relate to how to finish migrating authentication support. + +== Propagate ``AuthenticationServiceException``s + +{security-api-url}org/springframework/security/web/authentication/AuthenticationFilter.html[`AuthenticationFilter`] propagates {security-api-url}org/springframework/security/authentication/AuthenticationServiceException.html[``AuthenticationServiceException``]s to the {security-api-url}org/springframework/security/web/AuthenticationEntryPoint.html[`AuthenticationEntryPoint`]. +Because ``AuthenticationServiceException``s represent a server-side error instead of a client-side error, in 6.0, this changes to propagate them to the container. + +So, if you opted into this behavior by setting `rethrowAuthenticationServiceException` to `true`, you can now remove it like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +AuthenticationFilter authenticationFilter = new AuthenticationFilter(...); +AuthenticationEntryPointFailureHandler handler = new AuthenticationEntryPointFailureHandler(...); +handler.setRethrowAuthenticationServiceException(true); +authenticationFilter.setAuthenticationFailureHandler(handler); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +val authenticationFilter: AuthenticationFilter = AuthenticationFilter(...) +val handler: AuthenticationEntryPointFailureHandler = AuthenticationEntryPointFailureHandler(...) +handler.setRethrowAuthenticationServiceException(true) +authenticationFilter.setAuthenticationFailureHandler(handler) +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + + + + + + + + +---- +====== + +changes to: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +AuthenticationFilter authenticationFilter = new AuthenticationFilter(...); +AuthenticationEntryPointFailureHandler handler = new AuthenticationEntryPointFailureHandler(...); +authenticationFilter.setAuthenticationFailureHandler(handler); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +val authenticationFilter: AuthenticationFilter = AuthenticationFilter(...) +val handler: AuthenticationEntryPointFailureHandler = AuthenticationEntryPointFailureHandler(...) +authenticationFilter.setAuthenticationFailureHandler(handler) +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + + + + + + + + +---- +====== + +[[servlet-opt-in-sha256-rememberme]] +== Use SHA-256 in Remember Me + +In 6.0, the `TokenBasedRememberMeServices` uses SHA-256 to encode and match the token. +To complete the migration, any default values can be removed. + +For example, if you opted in to the 6.0 default for `encodingAlgorithm` and `matchingAlgorithm` like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableWebSecurity +public class SecurityConfig { + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception { + http + // ... + .rememberMe((remember) -> remember + .rememberMeServices(rememberMeServices) + ); + return http.build(); + } + @Bean + RememberMeServices rememberMeServices(UserDetailsService userDetailsService) { + RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.SHA256; + TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService, encodingAlgorithm); + rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.SHA256); + return rememberMe; + } +} +---- + +XML:: ++ +[source,xml,role="secondary"] +---- + + + + + + + + + +---- +====== + +then the defaults can be removed: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableWebSecurity +public class SecurityConfig { + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception { + http + // ... + .rememberMe((remember) -> remember + .rememberMeServices(rememberMeServices) + ); + return http.build(); + } + @Bean + RememberMeServices rememberMeServices(UserDetailsService userDetailsService) { + return new TokenBasedRememberMeServices(myKey, userDetailsService); + } +} +---- + +XML:: ++ +[source,xml,role="secondary"] +---- + + + + + + + +---- +====== + +== Default authorities for oauth2Login() + +In Spring Security 5, the default `GrantedAuthority` given to a user that authenticates with an OAuth2 or OpenID Connect 1.0 provider (via `oauth2Login()`) is `ROLE_USER`. + +In Spring Security 6, the default authority given to a user authenticating with an OAuth2 provider is `OAUTH2_USER`. +The default authority given to a user authenticating with an OpenID Connect 1.0 provider is `OIDC_USER`. +If you configured the `GrantedAuthoritiesMapper` only for the purpose of updating to 6.0, you can remove it completely. diff --git a/docs/modules/ROOT/pages/migration/servlet/authorization.adoc b/docs/modules/ROOT/pages/migration/servlet/authorization.adoc new file mode 100644 index 0000000000..217aecfdb9 --- /dev/null +++ b/docs/modules/ROOT/pages/migration/servlet/authorization.adoc @@ -0,0 +1,117 @@ += Authorization Migrations + +The following steps relate to how to finish migrating authorization support. + +== Use `AuthorizationManager` for Method Security + +There are no further migration steps for this feature. + +== Use `AuthorizationManager` for Message Security + +In 6.0, `` defaults `use-authorization-manager` to `true`. +So, to complete migration, remove any `websocket-message-broker@use-authorization-manager=true` attribute. + +For example: + +[tabs] +====== +Xml:: ++ +[source,xml,role="primary"] +---- + +---- +====== + +changes to: + +[tabs] +====== +Xml:: ++ +[source,xml,role="primary"] +---- + +---- +====== + +There are no further migrations steps for Java or Kotlin for this feature. + +== Use `AuthorizationManager` for Request Security + +In 6.0, `` defaults `once-per-request` to `false`, `filter-all-dispatcher-types` to `true`, and `use-authorization-manager` to `true`. +Also, xref:servlet/authorization/authorize-requests.adoc#filtersecurityinterceptor-every-request[`authorizeRequests#filterSecurityInterceptorOncePerRequest`] defaults to `false` and xref:servlet/authorization/authorize-http-requests.adoc[`authorizeHttpRequests#filterAllDispatcherTypes`] defaults to `true`. +So, to complete migration, any defaults values can be removed. + +For example, if you opted in to the 6.0 default for `filter-all-dispatcher-types` or `authorizeHttpRequests#filterAllDispatcherTypes` like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +http + .authorizeHttpRequests((authorize) -> authorize + .filterAllDispatcherTypes(true) + // ... + ) +---- + +Kotlin:: ++ +[source,java,role="secondary"] +---- +http { + authorizeHttpRequests { + filterAllDispatcherTypes = true + // ... + } +} +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + +---- +====== + +then the defaults may be removed: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +http + .authorizeHttpRequests((authorize) -> authorize + // ... + ) +---- + +Kotlin:: ++ +[source,java,role="secondary"] +---- +http { + authorizeHttpRequests { + // ... + } +} +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + +---- +====== + +[NOTE] +==== +`once-per-request` applies only when `use-authorization-manager="false"` and `filter-all-dispatcher-types` only applies when `use-authorization-manager="true"` +==== diff --git a/docs/modules/ROOT/pages/migration/servlet/exploits.adoc b/docs/modules/ROOT/pages/migration/servlet/exploits.adoc new file mode 100644 index 0000000000..bddafec6c2 --- /dev/null +++ b/docs/modules/ROOT/pages/migration/servlet/exploits.adoc @@ -0,0 +1,44 @@ += Exploit Protection Migrations +:spring-security-reference-base-url: https://docs.spring.io/spring-security/reference + +The 5.8 migration guide contains several steps for +ifdef::spring-security-version[] +{spring-security-reference-base-url}/5.8/migration/servlet/exploits.html[exploit protection migrations] when updating to 6.0. +endif::[] +ifndef::spring-security-version[] +exploit protection migrations when updating to 6.0. +endif::[] +You are encouraged to follow those steps first. + +The following steps relate to how to finish migrating exploit protection support. + +== Defer Loading CsrfToken + +In Spring Security 5.8, the default `CsrfTokenRequestHandler` for making the `CsrfToken` available to the application is `CsrfTokenRequestAttributeHandler`. +The default for the field `csrfRequestAttributeName` is `null`, which causes the CSRF token to be loaded on every request. + +In Spring Security 6, `csrfRequestAttributeName` defaults to `_csrf`. +If you configured the following only for the purpose of updating to 6.0, you can now remove it: + + requestHandler.setCsrfRequestAttributeName("_csrf"); + +== Protect against CSRF BREACH + +In Spring Security 5.8, the default `CsrfTokenRequestHandler` for making the `CsrfToken` available to the application is `CsrfTokenRequestAttributeHandler`. +`XorCsrfTokenRequestAttributeHandler` was added to allow opting into CSRF BREACH support. + +In Spring Security 6, `XorCsrfTokenRequestAttributeHandler` is the default `CsrfTokenRequestHandler` for making the `CsrfToken` available. +If you configured the `XorCsrfTokenRequestAttributeHandler` only for the purpose of updating to 6.0, you can remove it completely. + +[NOTE] +==== +If you have set the `csrfRequestAttributeName` to `null` in order to opt out of deferred tokens, or if you have configured a `CsrfTokenRequestHandler` for any other reason, you can leave the configuration in place. +==== + +== CSRF BREACH with WebSocket support + +In Spring Security 5.8, the default `ChannelInterceptor` for making the `CsrfToken` available with xref:servlet/integrations/websocket.adoc[WebSocket Security] is `CsrfChannelInterceptor`. +`XorCsrfChannelInterceptor` was added to allow opting into CSRF BREACH support. + +In Spring Security 6, `XorCsrfChannelInterceptor` is the default `ChannelInterceptor` for making the `CsrfToken` available. +If you configured the `XorCsrfChannelInterceptor` only for the purpose of updating to 6.0, you can remove it completely. diff --git a/docs/modules/ROOT/pages/migration/servlet/index.adoc b/docs/modules/ROOT/pages/migration/servlet/index.adoc new file mode 100644 index 0000000000..adc75685a6 --- /dev/null +++ b/docs/modules/ROOT/pages/migration/servlet/index.adoc @@ -0,0 +1,4 @@ += Servlet Migrations +:page-section-summary-toc: 1 + +If you have already performed the xref:migration/index.adoc[initial migration steps] for your Servlet application, you're now ready to perform steps specific to Servlet applications. diff --git a/docs/modules/ROOT/pages/migration/servlet/session-management.adoc b/docs/modules/ROOT/pages/migration/servlet/session-management.adoc new file mode 100644 index 0000000000..c7409b9e07 --- /dev/null +++ b/docs/modules/ROOT/pages/migration/servlet/session-management.adoc @@ -0,0 +1,49 @@ += Session Management Migrations + +The following steps relate to how to finish migrating session management support. + +== Require Explicit Saving of SecurityContextRepository + +In Spring Security 5, the default behavior is for the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontext[`SecurityContext`] to automatically be saved to the xref:servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`] using the xref:servlet/authentication/persistence.adoc#securitycontextpersistencefilter[`SecurityContextPersistenceFilter`]. +Saving must be done just prior to the `HttpServletResponse` being committed and just before `SecurityContextPersistenceFilter`. +Unfortunately, automatic persistence of the `SecurityContext` can surprise users when it is done prior to the request completing (i.e. just prior to committing the `HttpServletResponse`). +It also is complex to keep track of the state to determine if a save is necessary causing unnecessary writes to the `SecurityContextRepository` (i.e. `HttpSession`) at times. + +In Spring Security 6, the default behavior is that the xref:servlet/authentication/persistence.adoc#securitycontextholderfilter[`SecurityContextHolderFilter`] will only read the `SecurityContext` from `SecurityContextRepository` and populate it in the `SecurityContextHolder`. +Users now must explicitly save the `SecurityContext` with the `SecurityContextRepository` if they want the `SecurityContext` to persist between requests. +This removes ambiguity and improves performance by only requiring writing to the `SecurityContextRepository` (i.e. `HttpSession`) when it is necessary. + +[NOTE] +==== +Saving the context is also needed when clearing it out, for example during logout. Refer to this section to xref:servlet/authentication/session-management.adoc#properly-clearing-authentication[know more about that]. +==== + +If you are explicitly opting into Spring Security 6's new defaults, the following configuration can be removed to accept the Spring Security 6 defaults. + + +include::partial$servlet/architecture/security-context-explicit.adoc[] + +== Multiple SecurityContextRepository + +In Spring Security 5, the default xref:servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`] was `HttpSessionSecurityContextRepository`. + +In Spring Security 6, the default `SecurityContextRepository` is `DelegatingSecurityContextRepository`. +If you configured the `SecurityContextRepository` only for the purpose of updating to 6.0, you can remove it completely. + +== Deprecation in SecurityContextRepository + +There are no further migration steps for this deprecation. + +[[requestcache-query-optimization]] +== Optimize Querying of `RequestCache` + +In Spring Security 5, the default behavior is to query the xref:servlet/architecture.adoc#savedrequests[saved request] on every request. +This means that in a typical setup, that in order to use the xref:servlet/architecture.adoc#requestcache[`RequestCache`] the `HttpSession` is queried on every request. + +In Spring Security 6, the default is that `RequestCache` will only be queried for a cached request if the HTTP parameter `continue` is defined. +This allows Spring Security to avoid unnecessarily reading the `HttpSession` with the `RequestCache`. + +In Spring Security 5 the default is to use `HttpSessionRequestCache` which will be queried for a cached request on every request. +If you are not overriding the defaults (i.e. using `NullRequestCache`), then the following configuration can be used to explicitly opt into the Spring Security 6 behavior in Spring Security 5.8: + +include::partial$servlet/architecture/request-cache-continue.adoc[] diff --git a/docs/modules/ROOT/pages/reactive/integrations/observability.adoc b/docs/modules/ROOT/pages/reactive/integrations/observability.adoc index 27b533e6a5..d489cfcf89 100644 --- a/docs/modules/ROOT/pages/reactive/integrations/observability.adoc +++ b/docs/modules/ROOT/pages/reactive/integrations/observability.adoc @@ -197,8 +197,8 @@ Java:: ---- @Bean ObservationRegistryCustomizer noSpringSecurityObservations() { - ObservationPredicate predicate = (name, context) -> !name.startsWith("spring.security."); - return (registry) -> registry.observationConfig().observationPredicate(predicate); + ObservationPredicate predicate = (name, context) -> !name.startsWith("spring.security.") + return (registry) -> registry.observationConfig().observationPredicate(predicate) } ---- diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc index a0c2d5bc9c..b18c749f09 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc @@ -41,7 +41,6 @@ public class OAuth2ClientSecurityConfig { .clientRegistrationRepository(this.clientRegistrationRepository()) .authorizedClientRepository(this.authorizedClientRepository()) .authorizationRequestRepository(this.authorizationRequestRepository()) - .authorizationRequestResolver(this.authorizationRequestResolver()) .authenticationConverter(this.authenticationConverter()) .authenticationManager(this.authenticationManager()) ); @@ -66,7 +65,6 @@ class OAuth2ClientSecurityConfig { clientRegistrationRepository = clientRegistrationRepository() authorizedClientRepository = authorizedClientRepository() authorizationRequestRepository = authorizedRequestRepository() - authorizationRequestResolver = authorizationRequestResolver() authenticationConverter = authenticationConverter() authenticationManager = authenticationManager() } diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc index 319cba192b..0035fd2ec5 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc @@ -700,5 +700,111 @@ For MAC based algorithms such as `HS256`, `HS384` or `HS512`, the `client-secret [TIP] If more than one `ClientRegistration` is configured for OpenID Connect 1.0 Authentication, the JWS algorithm resolver may evaluate the provided `ClientRegistration` to determine which algorithm to return. + [[webflux-oauth2-login-advanced-oidc-logout]] -Then, you can proceed to configure xref:reactive/oauth2/login/logout.adoc[logout]. +== OpenID Connect 1.0 Logout + +OpenID Connect Session Management 1.0 allows the ability to log out the End-User at the Provider using the Client. +One of the strategies available is https://openid.net/specs/openid-connect-rpinitiated-1_0.html[RP-Initiated Logout]. + +If the OpenID Provider supports both Session Management and https://openid.net/specs/openid-connect-discovery-1_0.html[Discovery], the client may obtain the `end_session_endpoint` `URL` from the OpenID Provider's https://openid.net/specs/openid-connect-session-1_0.html#OPMetadata[Discovery Metadata]. +This can be achieved by configuring the `ClientRegistration` with the `issuer-uri`, as in the following example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + ... + provider: + okta: + issuer-uri: https://dev-1234.oktapreview.com +---- + +...and the `OidcClientInitiatedServerLogoutSuccessHandler`, which implements RP-Initiated Logout, may be configured as follows: + +[tabs] +====== +Java:: ++ +[source,java,role="primary",subs="-attributes"] +---- +@Configuration +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Autowired + private ReactiveClientRegistrationRepository clientRegistrationRepository; + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()) + .logout(logout -> logout + .logoutSuccessHandler(oidcLogoutSuccessHandler()) + ); + + return http.build(); + } + + private ServerLogoutSuccessHandler oidcLogoutSuccessHandler() { + OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler = + new OidcClientInitiatedServerLogoutSuccessHandler(this.clientRegistrationRepository); + + // Sets the location that the End-User's User Agent will be redirected to + // after the logout has been performed at the Provider + oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}"); + + return oidcLogoutSuccessHandler; + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary",subs="-attributes"] +---- +@Configuration +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Autowired + private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + logout { + logoutSuccessHandler = oidcLogoutSuccessHandler() + } + } + + return http.build() + } + + private fun oidcLogoutSuccessHandler(): ServerLogoutSuccessHandler { + val oidcLogoutSuccessHandler = OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository) + + // Sets the location that the End-User's User Agent will be redirected to + // after the logout has been performed at the Provider + oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}") + return oidcLogoutSuccessHandler + } +} +---- +====== + +NOTE: `OidcClientInitiatedServerLogoutSuccessHandler` supports the `+{baseUrl}+` placeholder. +If used, the application's base URL, like `https://app.example.org`, will replace it at request time. diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc deleted file mode 100644 index ac1eb76028..0000000000 --- a/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc +++ /dev/null @@ -1,267 +0,0 @@ -= OIDC Logout - -Once an end user is able to login to your application, it's important to consider how they will log out. - -Generally speaking, there are three use cases for you to consider: - -1. I want to perform only a local logout -2. I want to log out both my application and the OIDC Provider, initiated by my application -3. I want to log out both my application and the OIDC Provider, initiated by the OIDC Provider - -[[configure-local-logout]] -== Local Logout - -To perform a local logout, no special OIDC configuration is needed. -Spring Security automatically stands up a local logout endpoint, which you can xref:reactive/authentication/logout.adoc[configure through the `logout()` DSL]. - -[[configure-client-initiated-oidc-logout]] -[[oauth2login-advanced-oidc-logout]] -== OpenID Connect 1.0 Client-Initiated Logout - -OpenID Connect Session Management 1.0 allows the ability to log out the end user at the Provider by using the Client. -One of the strategies available is https://openid.net/specs/openid-connect-rpinitiated-1_0.html[RP-Initiated Logout]. - -If the OpenID Provider supports both Session Management and https://openid.net/specs/openid-connect-discovery-1_0.html[Discovery], the client can obtain the `end_session_endpoint` `URL` from the OpenID Provider's https://openid.net/specs/openid-connect-session-1_0.html#OPMetadata[Discovery Metadata]. -You can do so by configuring the `ClientRegistration` with the `issuer-uri`, as follows: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-secret: okta-client-secret - ... - provider: - okta: - issuer-uri: https://dev-1234.oktapreview.com ----- - -Also, you should configure `OidcClientInitiatedServerLogoutSuccessHandler`, which implements RP-Initiated Logout, as follows: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebFluxSecurity -public class OAuth2LoginSecurityConfig { - - @Autowired - private ReactiveClientRegistrationRepository clientRegistrationRepository; - - @Bean - public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception { - http - .authorizeExchange((authorize) -> authorize - .anyExchange().authenticated() - ) - .oauth2Login(withDefaults()) - .logout((logout) -> logout - .logoutSuccessHandler(oidcLogoutSuccessHandler()) - ); - return http.build(); - } - - private ServerLogoutSuccessHandler oidcLogoutSuccessHandler() { - OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler = - new OidcClientInitiatedServerLogoutSuccessHandler(this.clientRegistrationRepository); - - // Sets the location that the End-User's User Agent will be redirected to - // after the logout has been performed at the Provider - oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}"); - - return oidcLogoutSuccessHandler; - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -@EnableWebFluxSecurity -class OAuth2LoginSecurityConfig { - @Autowired - private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository - - @Bean - open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { - authorizeExchange { - authorize(anyExchange, authenticated) - } - oauth2Login { } - logout { - logoutSuccessHandler = oidcLogoutSuccessHandler() - } - } - return http.build() - } - - private fun oidcLogoutSuccessHandler(): ServerLogoutSuccessHandler { - val oidcLogoutSuccessHandler = OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository) - - // Sets the location that the End-User's User Agent will be redirected to - // after the logout has been performed at the Provider - oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}") - return oidcLogoutSuccessHandler - } -} ----- -====== - -[NOTE] -==== -`OidcClientInitiatedServerLogoutSuccessHandler` supports the `+{baseUrl}+` placeholder. -If used, the application's base URL, such as `https://app.example.org`, replaces it at request time. -==== - -[[configure-provider-initiated-oidc-logout]] -== OpenID Connect 1.0 Back-Channel Logout - -OpenID Connect Session Management 1.0 allows the ability to log out the end user at the Client by having the Provider make an API call to the Client. -This is referred to as https://openid.net/specs/openid-connect-backchannel-1_0.html[OIDC Back-Channel Logout]. - -To enable this, you can stand up the Back-Channel Logout endpoint in the DSL like so: - -[tabs] -====== -Java:: -+ -[source=java,role="primary"] ----- -@Bean -public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception { - http - .authorizeExchange((authorize) -> authorize - .anyExchange().authenticated() - ) - .oauth2Login(withDefaults()) - .oidcLogout((logout) -> logout - .backChannel(Customizer.withDefaults()) - ); - return http.build(); -} ----- - -Kotlin:: -+ -[source=kotlin,role="secondary"] ----- -@Bean -open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { - authorizeExchange { - authorize(anyExchange, authenticated) - } - oauth2Login { } - oidcLogout { - backChannel { } - } - } - return http.build() -} ----- -====== - -And that's it! - -This will stand up the endpoint `+/logout/connect/back-channel/{registrationId}+` which the OIDC Provider can request to invalidate a given session of an end user in your application. - -[NOTE] -`oidcLogout` requires that `oauth2Login` also be configured. - -[NOTE] -`oidcLogout` requires that the session cookie be called `JSESSIONID` in order to correctly log out each session through a backchannel. - -=== Back-Channel Logout Architecture - -Consider a `ClientRegistration` whose identifier is `registrationId`. - -The overall flow for a Back-Channel logout is like this: - -1. At login time, Spring Security correlates the ID Token, CSRF Token, and Provider Session ID (if any) to your application's session id in its `ReactiveOidcSessionStrategy` implementation. -2. Then at logout time, your OIDC Provider makes an API call to `/logout/connect/back-channel/registrationId` including a Logout Token that indicates either the `sub` (the End User) or the `sid` (the Provider Session ID) to logout. -3. Spring Security validates the token's signature and claims. -4. If the token contains a `sid` claim, then only the Client's session that correlates to that provider session is terminated. -5. Otherwise, if the token contains a `sub` claim, then all that Client's sessions for that End User are terminated. - -[NOTE] -Remember that Spring Security's OIDC support is multi-tenant. -This means that it will only terminate sessions whose Client matches the `aud` claim in the Logout Token. - -=== Customizing the OIDC Provider Session Strategy - -By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session. - -There are a number of circumstances, like a clustered application, where it would be nice to store this instead in a separate location, like a database. - -You can achieve this by configuring a custom `ReactiveOidcSessionStrategy`, like so: - -[tabs] -====== -Java:: -+ -[source=java,role="primary"] ----- -@Component -public final class MySpringDataOidcSessionStrategy implements OidcSessionStrategy { - private final OidcProviderSessionRepository sessions; - - // ... - - @Override - public void saveSessionInformation(OidcSessionInformation info) { - this.sessions.save(info); - } - - @Override - public OidcSessionInformation(String clientSessionId) { - return this.sessions.removeByClientSessionId(clientSessionId); - } - - @Override - public Iterable removeSessionInformation(OidcLogoutToken token) { - return token.getSessionId() != null ? - this.sessions.removeBySessionIdAndIssuerAndAudience(...) : - this.sessions.removeBySubjectAndIssuerAndAudience(...); - } -} ----- - -Kotlin:: -+ -[source=kotlin,role="secondary"] ----- -@Component -class MySpringDataOidcSessionStrategy: ReactiveOidcSessionStrategy { - val sessions: OidcProviderSessionRepository - - // ... - - @Override - fun saveSessionInformation(info: OidcSessionInformation): Mono { - return this.sessions.save(info) - } - - @Override - fun removeSessionInformation(clientSessionId: String): Mono { - return this.sessions.removeByClientSessionId(clientSessionId); - } - - @Override - fun removeSessionInformation(token: OidcLogoutToken): Flux { - return token.getSessionId() != null ? - this.sessions.removeBySessionIdAndIssuerAndAudience(...) : - this.sessions.removeBySubjectAndIssuerAndAudience(...); - } -} ----- -====== diff --git a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc index 519da49f77..1fe284f580 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/jwt.adoc @@ -165,13 +165,11 @@ Java:: + [source,java,role="primary"] ---- -import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope; - @Bean SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http .authorizeExchange(exchanges -> exchanges - .pathMatchers("/message/**").access(hasScope("message:read")) + .pathMatchers("/message/**").hasAuthority("SCOPE_message:read") .anyExchange().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -185,13 +183,11 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -import org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope - @Bean fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { return http { authorizeExchange { - authorize("/message/**", hasScope("message:read")) + authorize("/message/**", hasAuthority("SCOPE_message:read")) authorize(anyExchange, authenticated) } oauth2ResourceServer { @@ -340,7 +336,6 @@ This is handy when you need deeper configuration, such as < { algorithms.add(RS512); algorithms.add(ES512); @@ -519,7 +464,7 @@ Kotlin:: ---- @Bean fun jwtDecoder(): ReactiveJwtDecoder { - return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri) + return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithms { it.add(RS512) it.add(ES512) @@ -686,14 +631,12 @@ Java:: + [source,java,role="primary"] ---- -import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope; - @Bean SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http .authorizeExchange(exchanges -> exchanges - .mvcMatchers("/contacts/**").access(hasScope("contacts")) - .mvcMatchers("/messages/**").access(hasScope("messages")) + .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") + .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") .anyExchange().authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerSpec::jwt); @@ -705,14 +648,12 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -import org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope - @Bean fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { return http { authorizeExchange { - authorize("/contacts/**", hasScope("contacts")) - authorize("/messages/**", hasScope("messages")) + authorize("/contacts/**", hasAuthority("SCOPE_contacts")) + authorize("/messages/**", hasAuthority("SCOPE_messages")) authorize(anyExchange, authenticated) } oauth2ResourceServer { diff --git a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/multitenancy.adoc b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/multitenancy.adoc index 7dd3c6b5a1..9e91587996 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/multitenancy.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/multitenancy.adoc @@ -23,8 +23,8 @@ Java:: + [source,java,role="primary"] ---- -JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver - .fromTrustedIssuers("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo"); +JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver + ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo"); http .authorizeExchange(exchanges -> exchanges @@ -39,8 +39,7 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -val customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver - .fromTrustedIssuers("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo") +val customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo") return http { authorizeExchange { diff --git a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc index 8cab012346..bd622258ed 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/resource-server/opaque-token.adoc @@ -214,8 +214,6 @@ Java:: + [source,java,role="primary"] ---- -import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope; - @Configuration @EnableWebFluxSecurity public class MyCustomSecurityConfiguration { @@ -223,7 +221,7 @@ public class MyCustomSecurityConfiguration { SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http .authorizeExchange(exchanges -> exchanges - .pathMatchers("/messages/**").access(hasScope("message:read")) + .pathMatchers("/messages/**").hasAuthority("SCOPE_message:read") .anyExchange().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -240,13 +238,11 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -import org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope - @Bean fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { return http { authorizeExchange { - authorize("/messages/**", hasScope("message:read")) + authorize("/messages/**", hasAuthority("SCOPE_message:read")) authorize(anyExchange, authenticated) } oauth2ResourceServer { @@ -446,8 +442,6 @@ Java:: + [source,java,role="primary"] ---- -import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope; - @Configuration @EnableWebFluxSecurity public class MappedAuthorities { @@ -455,8 +449,8 @@ public class MappedAuthorities { SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http .authorizeExchange(exchange -> exchange - .pathMatchers("/contacts/**").access(hasScope("contacts")) - .pathMatchers("/messages/**").access(hasScope("messages")) + .pathMatchers("/contacts/**").hasAuthority("SCOPE_contacts") + .pathMatchers("/messages/**").hasAuthority("SCOPE_messages") .anyExchange().authenticated() ) .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken); @@ -469,14 +463,12 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -import org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope - @Bean fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { return http { authorizeExchange { - authorize("/contacts/**", hasScope("contacts")) - authorize("/messages/**", hasScope("messages")) + authorize("/contacts/**", hasAuthority("SCOPE_contacts")) + authorize("/messages/**", hasAuthority("SCOPE_messages")) authorize(anyExchange, authenticated) } oauth2ResourceServer { diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 86e85cd8ba..a6ad46b43a 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -163,7 +163,7 @@ Defaults to `true`. [[nsa-http-use-expressions]] * **use-expressions** -Enables EL-expressions in the `access` attribute, as described in the chapter on xref:servlet/authorization/authorize-http-requests.adoc#authorization-expressions[expression-based access-control]. +Enables EL-expressions in the `access` attribute, as described in the chapter on xref:servlet/authorization/expression-based.adoc#el-access-web[expression-based access-control]. The default value is true. diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc index 4460fef52a..4f36c2c2cb 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc @@ -6,4 +6,4 @@ This appendix provides a reference to the elements available in the security nam If you haven't used the namespace before, please read the xref:servlet/configuration/xml-namespace.adoc#ns-config[introductory chapter] on namespace configuration, as this is intended as a supplement to the information there. Using a good quality XML editor while editing a configuration based on the schema is recommended as this will provide contextual information on which elements and attributes are available as well as comments explaining their purpose. The namespace is written in https://relaxng.org/[RELAX NG] Compact format and later converted into an XSD schema. -If you are familiar with this format, you may wish to examine the https://raw.githubusercontent.com/spring-projects/spring-security/main/config/src/main/resources/org/springframework/security/config/spring-security-6.2.rnc[schema file] directly. +If you are familiar with this format, you may wish to examine the https://raw.githubusercontent.com/spring-projects/spring-security/main/config/src/main/resources/org/springframework/security/config/spring-security-5.6.rnc[schema file] directly. diff --git a/docs/modules/ROOT/pages/servlet/architecture.adoc b/docs/modules/ROOT/pages/servlet/architecture.adoc index e7f557269e..85623738fa 100644 --- a/docs/modules/ROOT/pages/servlet/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/architecture.adoc @@ -76,27 +76,30 @@ The following listing shows pseudo code of `DelegatingFilterProxy`: ====== Java:: + -[source,java,role="primary"] +[source,java,role="primary",subs="+quotes,+macros"] ---- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { - Filter delegate = getFilterBean(someBeanName); // <1> - delegate.doFilter(request, response); // <2> + // Lazily get Filter that was registered as a Spring Bean + // For the example in <> `delegate` is an instance of __Bean Filter~0~__ + Filter delegate = getFilterBean(someBeanName); + // delegate work to the Spring Bean + delegate.doFilter(request, response); } ---- Kotlin:: + -[source,kotlin,role="secondary"] +[source,kotlin,role="secondary",subs="+quotes,+macros"] ---- fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { - val delegate: Filter = getFilterBean(someBeanName) // <1> - delegate.doFilter(request, response) // <2> + // Lazily get Filter that was registered as a Spring Bean + // For the example in <> `delegate` is an instance of __Bean Filter~0~__ + val delegate: Filter = getFilterBean(someBeanName) + // delegate work to the Spring Bean + delegate.doFilter(request, response) } ---- ====== -<1> Lazily get Filter that was registered as a Spring Bean. -For the example in <> `delegate` is an instance of __Bean Filter~0~__. -<2> Delegate work to the Spring Bean. Another benefit of `DelegatingFilterProxy` is that it allows delaying looking up `Filter` bean instances. This is important because the container needs to register the `Filter` instances before the container can start up. diff --git a/docs/modules/ROOT/pages/servlet/authentication/cas.adoc b/docs/modules/ROOT/pages/servlet/authentication/cas.adoc deleted file mode 100644 index 59125bde84..0000000000 --- a/docs/modules/ROOT/pages/servlet/authentication/cas.adoc +++ /dev/null @@ -1,466 +0,0 @@ -[[servlet-cas]] -= CAS Authentication - -[[cas-overview]] -== Overview -JA-SIG produces an enterprise-wide single sign on system known as CAS. -Unlike other initiatives, JA-SIG's Central Authentication Service is open source, widely used, simple to understand, platform independent, and supports proxy capabilities. -Spring Security fully supports CAS, and provides an easy migration path from single-application deployments of Spring Security through to multiple-application deployments secured by an enterprise-wide CAS server. - -You can learn more about CAS at https://www.apereo.org. -You will also need to visit this site to download the CAS Server files. - -[[cas-how-it-works]] -== How CAS Works -Whilst the CAS web site contains documents that detail the architecture of CAS, we present the general overview again here within the context of Spring Security. -Spring Security 3.x supports CAS 3. -At the time of writing, the CAS server was at version 3.4. - -Somewhere in your enterprise you will need to setup a CAS server. -The CAS server is simply a standard WAR file, so there isn't anything difficult about setting up your server. -Inside the WAR file you will customise the login and other single sign on pages displayed to users. - -When deploying a CAS 3.4 server, you will also need to specify an `AuthenticationHandler` in the `deployerConfigContext.xml` included with CAS. -The `AuthenticationHandler` has a simple method that returns a boolean as to whether a given set of Credentials is valid. -Your `AuthenticationHandler` implementation will need to link into some type of backend authentication repository, such as an LDAP server or database. -CAS itself includes numerous ``AuthenticationHandler``s out of the box to assist with this. -When you download and deploy the server war file, it is set up to successfully authenticate users who enter a password matching their username, which is useful for testing. - -Apart from the CAS server itself, the other key players are of course the secure web applications deployed throughout your enterprise. -These web applications are known as "services". -There are three types of services. -Those that authenticate service tickets, those that can obtain proxy tickets, and those that authenticate proxy tickets. -Authenticating a proxy ticket differs because the list of proxies must be validated and often times a proxy ticket can be reused. - - -[[cas-sequence]] -=== Spring Security and CAS Interaction Sequence -The basic interaction between a web browser, CAS server and a Spring Security-secured service is as follows: - -* The web user is browsing the service's public pages. -CAS or Spring Security is not involved. -* The user eventually requests a page that is either secure or one of the beans it uses is secure. -Spring Security's `ExceptionTranslationFilter` will detect the `AccessDeniedException` or `AuthenticationException`. -* Because the user's `Authentication` object (or lack thereof) caused an `AuthenticationException`, the `ExceptionTranslationFilter` will call the configured `AuthenticationEntryPoint`. -If using CAS, this will be the `CasAuthenticationEntryPoint` class. -* The `CasAuthenticationEntryPoint` will redirect the user's browser to the CAS server. -It will also indicate a `service` parameter, which is the callback URL for the Spring Security service (your application). -For example, the URL to which the browser is redirected might be https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas. -* After the user's browser redirects to CAS, they will be prompted for their username and password. -If the user presents a session cookie which indicates they've previously logged on, they will not be prompted to login again (there is an exception to this procedure, which we'll cover later). -CAS will use the `PasswordHandler` (or `AuthenticationHandler` if using CAS 3.0) discussed above to decide whether the username and password is valid. -* Upon successful login, CAS will redirect the user's browser back to the original service. -It will also include a `ticket` parameter, which is an opaque string representing the "service ticket". -Continuing our earlier example, the URL the browser is redirected to might be https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ. -* Back in the service web application, the `CasAuthenticationFilter` is always listening for requests to `/login/cas` (this is configurable, but we'll use the defaults in this introduction). -The processing filter will construct a `UsernamePasswordAuthenticationToken` representing the service ticket. -The principal will be equal to `CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER`, whilst the credentials will be the service ticket opaque value. -This authentication request will then be handed to the configured `AuthenticationManager`. -* The `AuthenticationManager` implementation will be the `ProviderManager`, which is in turn configured with the `CasAuthenticationProvider`. -The `CasAuthenticationProvider` only responds to ``UsernamePasswordAuthenticationToken``s containing the CAS-specific principal (such as `CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER`) and ``CasAuthenticationToken``s (discussed later). -* `CasAuthenticationProvider` will validate the service ticket using a `TicketValidator` implementation. -This will typically be a `Cas20ServiceTicketValidator` which is one of the classes included in the CAS client library. -In the event the application needs to validate proxy tickets, the `Cas20ProxyTicketValidator` is used. -The `TicketValidator` makes an HTTPS request to the CAS server in order to validate the service ticket. -It may also include a proxy callback URL, which is included in this example: https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptor. -* Back on the CAS server, the validation request will be received. -If the presented service ticket matches the service URL the ticket was issued to, CAS will provide an affirmative response in XML indicating the username. -If any proxy was involved in the authentication (discussed below), the list of proxies is also included in the XML response. -* [OPTIONAL] If the request to the CAS validation service included the proxy callback URL (in the `pgtUrl` parameter), CAS will include a `pgtIou` string in the XML response. -This `pgtIou` represents a proxy-granting ticket IOU. -The CAS server will then create its own HTTPS connection back to the `pgtUrl`. -This is to mutually authenticate the CAS server and the claimed service URL. -The HTTPS connection will be used to send a proxy granting ticket to the original web application. -For example, https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH. -* The `Cas20TicketValidator` will parse the XML received from the CAS server. -It will return to the `CasAuthenticationProvider` a `TicketResponse`, which includes the username (mandatory), proxy list (if any were involved), and proxy-granting ticket IOU (if the proxy callback was requested). -* Next `CasAuthenticationProvider` will call a configured `CasProxyDecider`. -The `CasProxyDecider` indicates whether the proxy list in the `TicketResponse` is acceptable to the service. -Several implementations are provided with Spring Security: `RejectProxyTickets`, `AcceptAnyCasProxy` and `NamedCasProxyDecider`. -These names are largely self-explanatory, except `NamedCasProxyDecider` which allows a `List` of trusted proxies to be provided. -* `CasAuthenticationProvider` will next request a `AuthenticationUserDetailsService` to load the `GrantedAuthority` objects that apply to the user contained in the `Assertion`. -* If there were no problems, `CasAuthenticationProvider` constructs a `CasAuthenticationToken` including the details contained in the `TicketResponse` and the ``GrantedAuthority``s. -* Control then returns to `CasAuthenticationFilter`, which places the created `CasAuthenticationToken` in the security context. -* The user's browser is redirected to the original page that caused the `AuthenticationException` (or a custom destination depending on the configuration). - -It's good that you're still here! -Let's now look at how this is configured - -[[cas-client]] -== Configuration of CAS Client -The web application side of CAS is made easy due to Spring Security. -It is assumed you already know the basics of using Spring Security, so these are not covered again below. -We'll assume a namespace based configuration is being used and add in the CAS beans as required. -Each section builds upon the previous section. -A full CAS sample application can be found in the Spring Security xref:samples.adoc#samples[Samples]. - - -[[cas-st]] -=== Service Ticket Authentication -This section describes how to setup Spring Security to authenticate Service Tickets. -Often times this is all a web application requires. -You will need to add a `ServiceProperties` bean to your application context. -This represents your CAS service: - -[source,xml] ----- - - - - ----- - -The `service` must equal a URL that will be monitored by the `CasAuthenticationFilter`. -The `sendRenew` defaults to false, but should be set to true if your application is particularly sensitive. -What this parameter does is tell the CAS login service that a single sign on login is unacceptable. -Instead, the user will need to re-enter their username and password in order to gain access to the service. - -The following beans should be configured to commence the CAS authentication process (assuming you're using a namespace configuration): - -[source,xml] ----- - -... - - - - - - - - - - - ----- - -For CAS to operate, the `ExceptionTranslationFilter` must have its `authenticationEntryPoint` property set to the `CasAuthenticationEntryPoint` bean. -This can easily be done using xref:servlet/appendix/namespace/http.adoc#nsa-http-entry-point-ref[entry-point-ref] as is done in the example above. -The `CasAuthenticationEntryPoint` must refer to the `ServiceProperties` bean (discussed above), which provides the URL to the enterprise's CAS login server. -This is where the user's browser will be redirected. - -The `CasAuthenticationFilter` has very similar properties to the `UsernamePasswordAuthenticationFilter` (used for form-based logins). -You can use these properties to customize things like behavior for authentication success and failure. - -Next you need to add a `CasAuthenticationProvider` and its collaborators: - -[source,xml,attrs="-attributes"] ----- - - - - - - - - - - - - - - - - - - - - - - -... - ----- - -The `CasAuthenticationProvider` uses a `UserDetailsService` instance to load the authorities for a user, once they have been authenticated by CAS. -We've shown a simple in-memory setup here. -Note that the `CasAuthenticationProvider` does not actually use the password for authentication, but it does use the authorities. - -The beans are all reasonably self-explanatory if you refer back to the <> section. - -This completes the most basic configuration for CAS. -If you haven't made any mistakes, your web application should happily work within the framework of CAS single sign on. -No other parts of Spring Security need to be concerned about the fact CAS handled authentication. -In the following sections we will discuss some (optional) more advanced configurations. - - -[[cas-singlelogout]] -=== Single Logout -The CAS protocol supports Single Logout and can be easily added to your Spring Security configuration. -Below are updates to the Spring Security configuration that handle Single Logout - -[source,xml] ----- - -... - - - - - - - - - - - - - - - - ----- - -The `logout` element logs the user out of the local application, but does not end the session with the CAS server or any other applications that have been logged into. -The `requestSingleLogoutFilter` filter will allow the URL of `/spring_security_cas_logout` to be requested to redirect the application to the configured CAS Server logout URL. -Then the CAS Server will send a Single Logout request to all the services that were signed into. -The `singleLogoutFilter` handles the Single Logout request by looking up the `HttpSession` in a static `Map` and then invalidating it. - -It might be confusing why both the `logout` element and the `singleLogoutFilter` are needed. -It is considered best practice to logout locally first since the `SingleSignOutFilter` just stores the `HttpSession` in a static `Map` in order to call invalidate on it. -With the configuration above, the flow of logout would be: - -* The user requests `/logout` which would log the user out of the local application and send the user to the logout success page. -* The logout success page, `/cas-logout.jsp`, should instruct the user to click a link pointing to `/logout/cas` in order to logout out of all applications. -* When the user clicks the link, the user is redirected to the CAS single logout URL (https://localhost:9443/cas/logout). -* On the CAS Server side, the CAS single logout URL then submits single logout requests to all the CAS Services. -On the CAS Service side, Apereo's `SingleSignOutFilter` processes the logout request by invalidating the original session. - - - -The next step is to add the following to your web.xml - -[source,xml] ----- - -characterEncodingFilter - - org.springframework.web.filter.CharacterEncodingFilter - - - encoding - UTF-8 - - - -characterEncodingFilter -/* - - - - org.apereo.cas.client.session.SingleSignOutHttpSessionListener - - ----- - -When using the SingleSignOutFilter you might encounter some encoding issues. -Therefore it is recommended to add the `CharacterEncodingFilter` to ensure that the character encoding is correct when using the `SingleSignOutFilter`. -Again, refer to Apereo CAS's documentation for details. -The `SingleSignOutHttpSessionListener` ensures that when an `HttpSession` expires, the mapping used for single logout is removed. - - -[[cas-pt-client]] -=== Authenticating to a Stateless Service with CAS -This section describes how to authenticate to a service using CAS. -In other words, this section discusses how to setup a client that uses a service that authenticates with CAS. -The next section describes how to setup a stateless service to Authenticate using CAS. - - -[[cas-pt-client-config]] -==== Configuring CAS to Obtain Proxy Granting Tickets -In order to authenticate to a stateless service, the application needs to obtain a proxy granting ticket (PGT). -This section describes how to configure Spring Security to obtain a PGT building upon thencas-st[Service Ticket Authentication] configuration. - -The first step is to include a `ProxyGrantingTicketStorage` in your Spring Security configuration. -This is used to store PGT's that are obtained by the `CasAuthenticationFilter` so that they can be used to obtain proxy tickets. -An example configuration is shown below - -[source,xml] ----- - - ----- - -The next step is to update the `CasAuthenticationProvider` to be able to obtain proxy tickets. -To do this replace the `Cas20ServiceTicketValidator` with a `Cas20ProxyTicketValidator`. -The `proxyCallbackUrl` should be set to a URL that the application will receive PGT's at. -Last, the configuration should also reference the `ProxyGrantingTicketStorage` so it can use a PGT to obtain proxy tickets. -You can find an example of the configuration changes that should be made below. - -[source,xml] ----- - -... - - - - - - - - ----- - -The last step is to update the `CasAuthenticationFilter` to accept PGT and to store them in the `ProxyGrantingTicketStorage`. -It is important the `proxyReceptorUrl` matches the `proxyCallbackUrl` of the `Cas20ProxyTicketValidator`. -An example configuration is shown below. - -[source,xml] ----- - - - ... - - - - ----- - -[[cas-pt-client-sample]] -==== Calling a Stateless Service Using a Proxy Ticket -Now that Spring Security obtains PGTs, you can use them to create proxy tickets which can be used to authenticate to a stateless service. -The CAS xref:samples.adoc#samples[sample application] contains a working example in the `ProxyTicketSampleServlet`. -Example code can be found below: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { -// NOTE: The CasAuthenticationToken can also be obtained using -// SecurityContextHolder.getContext().getAuthentication() -final CasAuthenticationToken token = (CasAuthenticationToken) request.getUserPrincipal(); -// proxyTicket could be reused to make calls to the CAS service even if the -// target url differs -final String proxyTicket = token.getAssertion().getPrincipal().getProxyTicketFor(targetUrl); - -// Make a remote call using the proxy ticket -final String serviceUrl = targetUrl+"?ticket="+URLEncoder.encode(proxyTicket, "UTF-8"); -String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8"); -... -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -protected fun doGet(request: HttpServletRequest, response: HttpServletResponse?) { - // NOTE: The CasAuthenticationToken can also be obtained using - // SecurityContextHolder.getContext().getAuthentication() - val token = request.userPrincipal as CasAuthenticationToken - // proxyTicket could be reused to make calls to the CAS service even if the - // target url differs - val proxyTicket = token.assertion.principal.getProxyTicketFor(targetUrl) - - // Make a remote call using the proxy ticket - val serviceUrl: String = targetUrl + "?ticket=" + URLEncoder.encode(proxyTicket, "UTF-8") - val proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8") -} ----- -====== - -[[cas-pt]] -=== Proxy Ticket Authentication -The `CasAuthenticationProvider` distinguishes between stateful and stateless clients. -A stateful client is considered any that submits to the `filterProcessesUrl` of the `CasAuthenticationFilter`. -A stateless client is any that presents an authentication request to `CasAuthenticationFilter` on a URL other than the `filterProcessesUrl`. - -Because remoting protocols have no way of presenting themselves within the context of an `HttpSession`, it isn't possible to rely on the default practice of storing the security context in the session between requests. -Furthermore, because the CAS server invalidates a ticket after it has been validated by the `TicketValidator`, presenting the same proxy ticket on subsequent requests will not work. - -One obvious option is to not use CAS at all for remoting protocol clients. -However, this would eliminate many of the desirable features of CAS. -As a middle-ground, the `CasAuthenticationProvider` uses a `StatelessTicketCache`. -This is used solely for stateless clients which use a principal equal to `CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER`. -What happens is the `CasAuthenticationProvider` will store the resulting `CasAuthenticationToken` in the `StatelessTicketCache`, keyed on the proxy ticket. -Accordingly, remoting protocol clients can present the same proxy ticket and the `CasAuthenticationProvider` will not need to contact the CAS server for validation (aside from the first request). -Once authenticated, the proxy ticket could be used for URLs other than the original target service. - -This section builds upon the previous sections to accommodate proxy ticket authentication. -The first step is to specify to authenticate all artifacts as shown below. - -[source,xml] ----- - -... - - ----- - -The next step is to specify `serviceProperties` and the `authenticationDetailsSource` for the `CasAuthenticationFilter`. -The `serviceProperties` property instructs the `CasAuthenticationFilter` to attempt to authenticate all artifacts instead of only ones present on the `filterProcessesUrl`. -The `ServiceAuthenticationDetailsSource` creates a `ServiceAuthenticationDetails` that ensures the current URL, based upon the `HttpServletRequest`, is used as the service URL when validating the ticket. -The method for generating the service URL can be customized by injecting a custom `AuthenticationDetailsSource` that returns a custom `ServiceAuthenticationDetails`. - -[source,xml] ----- - -... - - - - - - - ----- - -You will also need to update the `CasAuthenticationProvider` to handle proxy tickets. -To do this replace the `Cas20ServiceTicketValidator` with a `Cas20ProxyTicketValidator`. -You will need to configure the `statelessTicketCache` and which proxies you want to accept. -You can find an example of the updates required to accept all proxies below. - -[source,xml] ----- - - -... - - - - - - - - - - - - - - - - - - - - - ----- diff --git a/docs/modules/ROOT/pages/servlet/authentication/index.adoc b/docs/modules/ROOT/pages/servlet/authentication/index.adoc index 580bec7355..bffe96b39e 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/index.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/index.adoc @@ -16,7 +16,6 @@ These sections focus on specific ways you may want to authenticate and point bac * xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd[Username and Password] - how to authenticate with a username/password * xref:servlet/oauth2/login/index.adoc#oauth2login[OAuth 2.0 Login] - OAuth 2.0 Log In with OpenID Connect and non-standard OAuth 2.0 Login (i.e. GitHub) * xref:servlet/saml2/index.adoc#servlet-saml2[SAML 2.0 Login] - SAML 2.0 Log In -* xref:servlet/authentication/cas.adoc#servlet-cas[Central Authentication Server (CAS)] - Central Authentication Server (CAS) Support * xref:servlet/authentication/rememberme.adoc#servlet-rememberme[Remember Me] - how to remember a user past session expiration * xref:servlet/authentication/jaas.adoc#servlet-jaas[JAAS Authentication] - authenticate with JAAS * xref:servlet/authentication/preauth.adoc#servlet-preauth[Pre-Authentication Scenarios] - authenticate with an external mechanism such as https://www.siteminder.com/[SiteMinder] or Java EE security but still use Spring Security for authorization and protection against common exploits. diff --git a/docs/modules/ROOT/pages/servlet/authentication/logout.adoc b/docs/modules/ROOT/pages/servlet/authentication/logout.adoc index ea84001731..54b27e0288 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/logout.adoc @@ -1,444 +1,154 @@ [[jc-logout]] = Handling Logouts -In an application where end users can xref:servlet/authentication/index.adoc[login], they should also be able to logout. +This section covers how to customize the handling of logouts. -By default, Spring Security stands up a `/logout` endpoint, so no additional code is necessary. - -The rest of this section covers a number of use cases for you to consider: - -* I want to <> -* I want to <> -* I want to know when I need to <> -* I want to <> when the user logs out -* I am using OAuth 2.0 and I want to xref:servlet/oauth2/login/advanced.adoc#oauth2login-advanced-oidc-logout[coordinate logout with an Authorization Server] -* I am using SAML 2.0 and I want to xref:servlet/saml2/logout.adoc[coordinate logout with an Identity Provider] -* I am using CAS and I want to xref:servlet/authentication/cas.adoc#cas-singlelogout[coordinate logout with an Identity Provider] - -[[logout-architecture]] [[logout-java-configuration]] -== Understanding Logout's Architecture +== Logout Java/Kotlin Configuration -When you include {spring-boot-reference-url}using.html#using.build-systems.starters[the `spring-boot-starter-security` dependency] or use the `@EnableWebSecurity` annotation, Spring Security will add its logout support and by default respond both to `GET /logout` and `POST /logout`. +When using the `{security-api-url}org/springframework/security/config/annotation/web/builders/HttpSecurity.html[HttpSecurity]` bean, logout capabilities are automatically applied. +The default is that accessing the URL `/logout` logs the user out by: -If you request `GET /logout`, then Spring Security displays a logout confirmation page. -Aside from providing a valuable double-checking mechanism for the user, it also provides a simple way to provide xref:servlet/exploits/csrf.adoc[the needed CSRF token] to `POST /logout`. +- Invalidating the HTTP Session +- Cleaning up any RememberMe authentication that was configured +- Clearing the `SecurityContextHolder` +- Clearing the `SecurityContextRepository` +- Redirecting to `/login?logout` -Please note that if xref:servlet/exploits/csrf.adoc[CSRF protection] is disabled in configuration, no logout confirmation page is shown to the user and the logout is performed directly. +Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements: -[TIP] -In your application it is not necessary to use `GET /logout` to perform a logout. -So long as xref:servlet/exploits/csrf.adoc[the needed CSRF token] is present in the request, your application can simply `POST /logout` to induce a logout. - -If you request `POST /logout`, then it will perform the following default operations using a series of {security-api-url}org/springframework/security/web/authentication/logout/LogoutHandler.html[``LogoutHandler``]s: - -- Invalidate the HTTP session ({security-api-url}org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[`SecurityContextLogoutHandler`]) -- Clear the xref:servlet/authentication/session-management.adoc#use-securitycontextholderstrategy[`SecurityContextHolderStrategy`] ({security-api-url}org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[`SecurityContextLogoutHandler`]) -- Clear the xref:servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`] ({security-api-url}org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[`SecurityContextLogoutHandler`]) -- Clean up any xref:servlet/authentication/rememberme.adoc[RememberMe authentication] (`TokenRememberMeServices` / `PersistentTokenRememberMeServices`) -- Clear out any saved xref:servlet/exploits/csrf.adoc[CSRF token] ({security-api-url}org/springframework/security/web/csrf/CsrfLogoutHandler.html[`CsrfLogoutHandler`]) -- xref:servlet/authentication/events.adoc[Fire] a `LogoutSuccessEvent` ({security-api-url}org/springframework/security/web/authentication/logout/LogoutSuccessEventPublishingLogoutHandler.html[`LogoutSuccessEventPublishingLogoutHandler`]) - -Once completed, then it will exercise its default {security-api-url}org/springframework/security/web/authentication/logout/LogoutSuccessHandler.html[`LogoutSuccessHandler`] which redirects to `/login?logout`. - -[[customizing-logout-uris]] -== Customizing Logout URIs - -Since the `LogoutFilter` appears before xref:servlet/authorization/authorize-http-requests.adoc[the `AuthorizationFilter`] in xref:servlet/architecture.adoc#servlet-filterchain-figure[the filter chain], it is not necessary by default to explicitly permit the `/logout` endpoint. -Thus, only <> that you create yourself generally require a `permitAll` configuration to be reachable. - -For example, if you want to simply change the URI that Spring Security is matching, you can do so in the `logout` DSL in following way: - -.Custom Logout Uri +.Logout Configuration [tabs] ====== Java:: + [source,java,role="primary"] ---- -http - .logout((logout) -> logout.logoutUrl("/my/logout/uri")) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -http { - logout { - logoutUrl = "/my/logout/uri" - } +public SecurityFilterChain filterChain(HttpSecurity http) { + http + .logout(logout -> logout // <1> + .logoutUrl("/my/logout") // <2> + .logoutSuccessUrl("/my/index") // <3> + .logoutSuccessHandler(logoutSuccessHandler) // <4> + .invalidateHttpSession(true) // <5> + .addLogoutHandler(logoutHandler) // <6> + .deleteCookies(cookieNamesToClear) // <7> + ) + ... } ---- -Xml:: -+ -[source,xml,role="secondary"] ----- - ----- -====== - -and no authorization changes are necessary since it simply adjusts the `LogoutFilter`. - -[[permit-logout-endpoints]] -However, if you stand up your own logout success endpoint (or in a rare case, <>), say using {spring-framework-reference-url}web.html#spring-web[Spring MVC], you will need to permit it in Spring Security. -This is because Spring MVC processes your request after Spring Security does. - -You can do this using `authorizeHttpRequests` or `` like so: - -.Custom Logout Endpoint -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -http - .authorizeHttpRequests((authorize) -> authorize - .requestMatchers("/my/success/endpoint").permitAll() - // ... - ) - .logout((logout) -> logout.logoutSuccessUrl("/my/success/endpoint")) ----- - Kotlin:: + [source,kotlin,role="secondary"] ----- -http { - authorizeHttpRequests { - authorize("/my/success/endpoint", permitAll) - } - logout { - logoutSuccessUrl = "/my/success/endpoint" +----- +open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + logout { // <1> + logoutUrl = "/my/logout" // <2> + logoutSuccessUrl = "/my/index" // <3> + logoutSuccessHandler = customLogoutSuccessHandler // <4> + invalidateHttpSession = true // <5> + addLogoutHandler(logoutHandler) // <6> + deleteCookies(cookieNamesToClear) // <7> + } } + // ... } ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - - - ----- +----- ====== -In this example, you tell the `LogoutFilter` to redirect to `/my/success/endpoint` when it is done. -And, you explicitly permit the `/my/success/endpoint` endpoint in xref:servlet/authorization/authorize-http-requests.adoc[the `AuthorizationFilter`]. - -Specifying it twice can be cumbersome, though. -If you are using Java configuration, you can instead set the `permitAll` property in the logout DSL like so: - -.Permitting Custom Logout Endpoints -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -http - .authorizeHttpRequests((authorize) -> authorize - // ... - ) - .logout((logout) -> logout - .logoutSuccessUrl("/my/success/endpoint") - .permitAll() - ) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -http - authorizeHttpRequests { - // ... - } - logout { - logoutSuccessUrl = "/my/success/endpoint" - permitAll = true - } ----- -====== - -which will add all logout URIs to the permit list for you. - -[[add-logout-handler]] -== Adding Clean-up Actions - -If you are using Java configuration, you can add clean up actions of your own by calling the `addLogoutHandler` method in the `logout` DSL, like so: - -.Custom Logout Handler -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -CookieClearingLogoutHandler cookies = new CookieClearingLogoutHandler("our-custom-cookie"); -http - .logout((logout) -> logout.addLogoutHandler(cookies)) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -http { - logout { - addLogoutHandler(CookieClearingLogoutHandler("our-custom-cookie")) - } -} ----- -====== +<1> Provides logout support. +<2> The URL that triggers log out to occur (the default is `/logout`). +If CSRF protection is enabled (the default), the request must also be a POST. +For more information, see {security-api-url}org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.html#logoutUrl-java.lang.String-[`logoutUrl(java.lang.String logoutUrl)`]. +<3> The URL to which to redirect after logout has occurred. +The default is `/login?logout`. +For more information, see {security-api-url}org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.html#logoutSuccessUrl-java.lang.String-[`logoutSuccessUrl(java.lang.String logoutSuccessUrl)`]. +<4> Let's you specify a custom `LogoutSuccessHandler`. +If this is specified, `logoutSuccessUrl()` is ignored. +For more information, see {security-api-url}org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.html#logoutSuccessHandler-org.springframework.security.web.authentication.logout.LogoutSuccessHandler-[`LogoutSuccessHandler`]. +<5> Specify whether to invalidate the `HttpSession` at the time of logout. +This is *true* by default. +Configures the `SecurityContextLogoutHandler` under the covers. +For more information, see {security-api-url}org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.html#invalidateHttpSession-boolean-[`invalidateHttpSession(boolean invalidateHttpSession)`]. +<6> Adds a `LogoutHandler`. +By default, `SecurityContextLogoutHandler` is added as the last `LogoutHandler`. +<7> Lets specifying the names of cookies be removed on logout success. +This is a shortcut for adding a `CookieClearingLogoutHandler` explicitly. [NOTE] -Because {security-api-url}org/springframework/security/web/authentication/logout/LogoutHandler.html[``LogoutHandler``]s are for the purposes of cleanup, they should not throw exceptions. +==== +Logouts can also be configured by using the XML Namespace notation. +See the documentation for the xref:servlet/appendix/namespace/http.adoc#nsa-logout[ logout element] in the Spring Security XML Namespace section for further details. +==== -[TIP] -Since {security-api-url}org/springframework/security/web/authentication/logout/LogoutHandler.html[`LogoutHandler`] is a functional interface, you can provide a custom one as a lambda. +Generally, to customize logout functionality, you can add +`{security-api-url}org/springframework/security/web/authentication/logout/LogoutHandler.html[LogoutHandler]` +or +`{security-api-url}org/springframework/security/web/authentication/logout/LogoutSuccessHandler.html[LogoutSuccessHandler]` +implementations. +For many common scenarios, these handlers are applied under the +covers when using the fluent API. -Some logout handler configurations are common enough that they are exposed directly in the `logout` DSL and `` element. -One example is configuring session invalidation and another is which additional cookies should be deleted. +[[ns-logout]] +== Logout XML Configuration +The `logout` element adds support for logging out by navigating to a particular URL. +The default logout URL is `/logout`, but you can set it to something else by setting the `logout-url` attribute. +You can find more information on other available attributes in the namespace appendix. -For example, you can configure the {security-api-url}org/springframework/security/web/authentication/logout/CookieClearingLogoutHandler.html[`CookieClearingLogoutHandler`] as seen above. +[[jc-logout-handler]] +== LogoutHandler -[[delete-cookies]] -Or you can instead set the appropriate configuration value like so: +Generally, `{security-api-url}org/springframework/security/web/authentication/logout/LogoutHandler.html[LogoutHandler]` +implementations indicate classes that are able to participate in logout handling. +They are expected to be invoked to perform necessary clean-up. +As a result, they should +not throw exceptions. +Spring Security provides various implementations: -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -http - .logout((logout) -> logout.deleteCookies("our-custom-cookie")) ----- +- {security-api-url}org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServices.html[PersistentTokenBasedRememberMeServices] +- {security-api-url}org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.html[TokenBasedRememberMeServices] +- {security-api-url}org/springframework/security/web/authentication/logout/CookieClearingLogoutHandler.html[CookieClearingLogoutHandler] +- {security-api-url}org/springframework/security/web/csrf/CsrfLogoutHandler.html[CsrfLogoutHandler] +- {security-api-url}org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[SecurityContextLogoutHandler] +- {security-api-url}org/springframework/security/web/authentication/logout/HeaderWriterLogoutHandler.html[HeaderWriterLogoutHandler] -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -http { - logout { - deleteCookies = "our-custom-cookie" - } -} ----- +See xref:servlet/authentication/rememberme.adoc#remember-me-impls[Remember-Me Interfaces and Implementations] for details. -Xml:: -+ -[source,kotlin,role="secondary"] ----- - - - ----- -====== +Instead of providing `LogoutHandler` implementations directly, the fluent API also provides shortcuts that provide the respective `LogoutHandler` implementations under the covers. +For example, `deleteCookies()` lets you specify the names of one or more cookies to be removed on logout success. +This is a shortcut compared to adding a `CookieClearingLogoutHandler`. -[NOTE] -Specifying that the `JSESSIONID` cookie is not necessary since {security-api-url}/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[`SecurityContextLogoutHandler`] removes it by virtue of invalidating the session. +[[jc-logout-success-handler]] +== LogoutSuccessHandler -[[clear-all-site-data]] -=== Using Clear-Site-Data to Log Out the User +The `LogoutSuccessHandler` is called after a successful logout by the `LogoutFilter`, to handle (for example) +redirection or forwarding to the appropriate destination. +Note that the interface is almost the same as the `LogoutHandler` but may raise an exception. -The `Clear-Site-Data` HTTP header is one that browsers support as an instruction to clear cookies, storage, and cache that belong to the owning website. -This is a handy and secure way to ensure that everything, including the session cookie, is cleaned up on logout. +Spring Security provides the following implementations: -You can add configure Spring Security to write the `Clear-Site-Data` header on logout like so: +- {security-api-url}org/springframework/security/web/authentication/logout/SimpleUrlLogoutSuccessHandler.html[SimpleUrlLogoutSuccessHandler] +- HttpStatusReturningLogoutSuccessHandler -.Using Clear-Site-Data -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter()); -http - .logout((logout) -> logout.addLogoutHandler(clearSiteData)) ----- +As mentioned earlier, you need not specify the `SimpleUrlLogoutSuccessHandler` directly. +Instead, the fluent API provides a shortcut by setting the `logoutSuccessUrl()`. +This sets up the `SimpleUrlLogoutSuccessHandler` under the covers. +The provided URL is redirected to after a logout has occurred. +The default is `/login?logout`. -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -val clearSiteData = HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter()) -http { - logout { - addLogoutHandler(clearSiteData) - } -} ----- -====== - -You give the `ClearSiteDataHeaderWriter` constructor the list of things that you want to be cleared out. - -The above configuration clears out all site data, but you can also configure it to remove just cookies like so: - -.Using Clear-Site-Data to Clear Cookies -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directives.COOKIES)); -http - .logout((logout) -> logout.addLogoutHandler(clearSiteData)) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -val clearSiteData = HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(Directives.COOKIES)) -http { - logout { - addLogoutHandler(clearSiteData) - } -} ----- -====== - -[[customizing-logout-success]] -== Customizing Logout Success - -While using `logoutSuccessUrl` will suffice for most cases, you may need to do something different from redirecting to a URL once logout is complete. -{security-api-url}org/springframework/security/web/authentication/logout/LogoutSuccessHandler.html[`LogoutSuccessHandler`] is the Spring Security component for customizing logout success actions. - -For example, instead of redirecting, you may want to only return a status code. -In this case, you can provide a success handler instance, like so: - -.Using Clear-Site-Data to Clear Cookies -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -http - .logout((logout) -> logout.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -http { - logout { - logoutSuccessHandler = HttpStatusReturningLogoutSuccessHandler() - } -} ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - - - ----- -====== - -[TIP] -Since {security-api-url}org/springframework/security/web/authentication/logout/LogoutSuccessHandler.html[`LogoutSuccessHandler`] is a functional interface, you can provide a custom one as a lambda. - -[[creating-custom-logout-endpoint]] -== Creating a Custom Logout Endpoint - -It is strongly recommended that you use the provided `logout` DSL to configure logout. -One reason is that its easy to forget to call the needed Spring Security components to ensure a proper and complete logout. - -In fact, it is often simpler to <> than create a {spring-framework-reference-url}web.html#spring-web[Spring MVC] endpoint for performing logout. - -That said, if you find yourself in a circumstance where a custom logout endpoint is needed, like the following one: - -.Custom Logout Endpoint -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@PostMapping("/my/logout") -public String performLogout() { - // .. perform logout - return "redirect:/home"; -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@PostMapping("/my/logout") -fun performLogout(): String { - // .. perform logout - return "redirect:/home" -} ----- -====== - -then you will need to have that endpoint invoke Spring Security's {security-api-url}/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[`SecurityContextLogoutHandler`] to ensure a secure and complete logout. -Something like the following is needed at a minimum: - -.Custom Logout Endpoint -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler(); - -@PostMapping("/my/logout") -public String performLogout(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { - // .. perform logout - this.logoutHandler.doLogout(request, response, authentication); - return "redirect:/home"; -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -val logoutHandler = SecurityContextLogoutHandler() - -@PostMapping("/my/logout") -fun performLogout(val authentication: Authentication, val request: HttpServletRequest, val response: HttpServletResponse): String { - // .. perform logout - this.logoutHandler.doLogout(request, response, authentication) - return "redirect:/home" -} ----- -====== - -Such will clear out the {security-api-url}/org/springframework/security/core/context/SecurityContextHolderStrategy.html[`SecurityContextHolderStrategy`] and {security-api-url}/org/springframework/security/web/context/SecurityContextRepository.html[`SecurityContextRepository`] as needed. - -Also, you'll need to <>. - -[WARNING] -Failing to call {security-api-url}/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[`SecurityContextLogoutHandler`] means that xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontext[the `SecurityContext`] could still be available on subsequent requests, meaning that the user is not actually logged out. - -[[testing-logout]] -== Testing Logout -Once you have logout configured you can test it using xref:servlet/test/mockmvc/logout.adoc[Spring Security's MockMvc support]. +The `HttpStatusReturningLogoutSuccessHandler` can be interesting in REST API type scenarios. +Instead of redirecting to a URL upon the successful logout, this `LogoutSuccessHandler` lets you provide a plain HTTP status code to be returned. +If not configured, a status code 200 is returned by default. [[jc-logout-references]] == Further Logout-Related References +- xref:servlet/authentication/session-management.adoc#properly-clearing-authentication[Properly Clearing Authentication When Explicit Save Is Enabled] +- <> - xref:servlet/test/mockmvc/logout.adoc#test-logout[Testing Logout] -- xref:servlet/integrations/servlet-api.adoc#servletapi-logout[HttpServletRequest.logout()] +- xref:servlet/integrations/servlet-api.adoc#servletapi-logout[`HttpServletRequest.logout()`] - xref:servlet/authentication/rememberme.adoc#remember-me-impls[Remember-Me Interfaces and Implementations] - xref:servlet/exploits/csrf.adoc#csrf-considerations-logout[Logging Out] in section CSRF Caveats -- Section xref:servlet/authentication/cas.adoc#cas-singlelogout[Single Logout] (CAS protocol) - Documentation for the xref:servlet/appendix/namespace/http.adoc#nsa-logout[logout element] in the Spring Security XML Namespace section diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc index aeecd5e60b..c401bb0997 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc @@ -15,7 +15,7 @@ The preceding figure builds off our xref:servlet/architecture.adoc#servlet-secur image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the resource `/private` for which it is not authorized. -image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`. +image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`. image:{icondir}/number_3.png[] Since the user is not authenticated, xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__. The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/web/authentication/www/BasicAuthenticationEntryPoint.html[`BasicAuthenticationEntryPoint`], which sends a WWW-Authenticate header. diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc index 91f211e3e6..8850fb179a 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/form.adoc @@ -16,7 +16,7 @@ The preceding figure builds off our xref:servlet/architecture.adoc#servlet-secur image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the resource (`/private`) for which it is not authorized. -image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`. +image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`. image:{icondir}/number_3.png[] Since the user is not authenticated, xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__ and sends a redirect to the login page with the configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`]. In most cases, the `AuthenticationEntryPoint` is an instance of {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[`LoginUrlAuthenticationEntryPoint`]. diff --git a/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc b/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc index f9434d3826..75f47de0fa 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/session-management.adoc @@ -80,7 +80,7 @@ If you try to use any of these methods, an exception will be thrown. By default, Spring Security stores the security context for you in the HTTP session. However, here are several reasons you may want to customize that: -* You may want to call individual setters on the `HttpSessionSecurityContextRepository` instance +* You may want call individual setters on the `HttpSessionSecurityContextRepository` instance * You may want to store the security context in a cache or database to enable horizontal scaling First, you need to create an implementation of `SecurityContextRepository` or use an existing implementation like `HttpSessionSecurityContextRepository`, then you can set it in `HttpSecurity`. @@ -192,7 +192,29 @@ If you are not sure what `securityContextHolderStrategy` is in the above example === Properly Clearing an Authentication If you are using Spring Security's xref:servlet/authentication/logout.adoc[Logout Support] then it handles a lot of stuff for you including clearing and saving the context. -But, let's say you need to manually log users out of your app. In that case, you'll need to make sure you're xref:servlet/authentication/logout.adoc#creating-custom-logout-endpoint[clearing and saving the context properly]. +But, let's say you need to manually log users out of your app. In that case, you'll need to make sure you're clearing and saving the context properly. + +Now, you might already be familiar with clearing the `SecurityContextHolder` by doing `SecurityContextHolderStrategy#clearContext()`. +That's great, but if your app requires an xref:migration/servlet/session-management.adoc#_require_explicit_saving_of_securitycontextrepository[explicit save of the context], simply clearing it isn't enough. +The reason is that it doesn't remove it from the `SecurityContextRepository`, which means the `SecurityContext` could still be available for the next requests, and we definitely don't want that. + +To make sure the authentication is properly cleared and saved, you can invoke {security-api-url}/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html[the `SecurityContextLogoutHandler`] which does that for us, like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +SecurityContextLogoutHandler handler = new SecurityContextLogoutHandler(); <1> +handler.logout(httpServletRequest, httpServletResponse, null); <2> +---- +====== + +<1> Create a new instance of `SecurityContextLogoutHandler` +<2> Call the `logout` method passing in the `HttpServletRequest`, `HttpServletResponse` and a `null` authentication because it is not required for this handler. + +It's important to remember that clearing and saving the context is just one piece of the logout process, therefore we recommend having Spring Security take care of it. [[stateless-authentication]] === Configuring Persistence for Stateless Authentication diff --git a/docs/modules/ROOT/pages/servlet/authorization/acls.adoc b/docs/modules/ROOT/pages/servlet/authorization/acls.adoc index b8c9189d7c..4d470ebdbb 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/acls.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/acls.adoc @@ -209,4 +209,4 @@ Such classes would use `AclService` to retrieve the relevant ACL and then call ` Alternately, you could use our `AclEntryVoter`, `AclEntryAfterInvocationProvider` or `AclEntryAfterInvocationCollectionFilteringProvider` classes. All of these classes provide a declarative-based approach to evaluating ACL information at runtime, freeing you from needing to write any code. -See the https://github.com/spring-projects/spring-security-samples[sample applications] to learn how to use these classes. +See the https://github.com/spring-projects/spring-security/tree/master/samples[sample applications] to learn how to use these classes. diff --git a/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc b/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc index adda11bb47..807a14798f 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc @@ -21,80 +21,30 @@ String getAuthority(); ---- -This method is used by an -`AuthorizationManager` instance to obtain a precise `String` representation of the `GrantedAuthority`. -By returning a representation as a `String`, a `GrantedAuthority` can be easily "read" by most `AuthorizationManager` implementations. -If a `GrantedAuthority` cannot be precisely represented as a `String`, the `GrantedAuthority` is considered "complex" and `getAuthority()` must return `null`. +This method lets an +`AccessDecisionManager` instance to obtain a precise `String` representation of the `GrantedAuthority`. +By returning a representation as a `String`, a `GrantedAuthority` can be easily "`read`" by most `AccessDecisionManager` implementations. +If a `GrantedAuthority` cannot be precisely represented as a `String`, the `GrantedAuthority` is considered "`complex`" and `getAuthority()` must return `null`. -An example of a complex `GrantedAuthority` would be an implementation that stores a list of operations and authority thresholds that apply to different customer account numbers. +An example of a "`complex`" `GrantedAuthority` would be an implementation that stores a list of operations and authority thresholds that apply to different customer account numbers. Representing this complex `GrantedAuthority` as a `String` would be quite difficult. As a result, the `getAuthority()` method should return `null`. -This indicates to any `AuthorizationManager` that it needs to support the specific `GrantedAuthority` implementation to understand its contents. +This indicates to any `AccessDecisionManager` that it needs to support the specific `GrantedAuthority` implementation to understand its contents. Spring Security includes one concrete `GrantedAuthority` implementation: `SimpleGrantedAuthority`. This implementation lets any user-specified `String` be converted into a `GrantedAuthority`. All `AuthenticationProvider` instances included with the security architecture use `SimpleGrantedAuthority` to populate the `Authentication` object. -[[jc-method-security-custom-granted-authority-defaults]] -By default, role-based authorization rules include `ROLE_` as a prefix. -This means that if there is an authorization rule that requires a security context to have a role of "USER", Spring Security will by default look for a `GrantedAuthority#getAuthority` that returns "ROLE_USER". - -You can customize this with `GrantedAuthorityDefaults`. -`GrantedAuthorityDefaults` exists to allow customizing the prefix to use for role-based authorization rules. - -You can configure the authorization rules to use a different prefix by exposing a `GrantedAuthorityDefaults` bean, like so: - -.Custom MethodSecurityExpressionHandler -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -static GrantedAuthorityDefaults grantedAuthorityDefaults() { - return new GrantedAuthorityDefaults("MYPREFIX_"); -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -companion object { - @Bean - fun grantedAuthorityDefaults() : GrantedAuthorityDefaults { - return GrantedAuthorityDefaults("MYPREFIX_"); - } -} ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - - ----- -====== - -[TIP] -==== -You expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes -==== - [[authz-pre-invocation]] -== Invocation Handling +== Pre-Invocation Handling Spring Security provides interceptors that control access to secure objects, such as method invocations or web requests. -A pre-invocation decision on whether the invocation is allowed to proceed is made by `AuthorizationManager` instances. -Also post-invocation decisions on whether a given value may be returned is made by `AuthorizationManager` instances. +A pre-invocation decision on whether the invocation is allowed to proceed is made by the `AccessDecisionManager`. === The AuthorizationManager `AuthorizationManager` supersedes both <>. Applications that customize an `AccessDecisionManager` or `AccessDecisionVoter` are encouraged to <>. -``AuthorizationManager``s are called by Spring Security's xref:servlet/authorization/authorize-http-requests.adoc[request-based], xref:servlet/authorization/method-security.adoc[method-based], and xref:servlet/integrations/websocket.adoc[message-based] authorization components and are responsible for making final access control decisions. +``AuthorizationManager``s are called by the xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] and are responsible for making final access control decisions. The `AuthorizationManager` interface contains two methods: [source,java] @@ -143,10 +93,6 @@ Another manager is the `AuthenticatedAuthorizationManager`. It can be used to differentiate between anonymous, fully-authenticated and remember-me authenticated users. Many sites allow certain limited access under remember-me authentication, but require a user to confirm their identity by logging in for full access. -[[authz-authorization-managers]] -==== AuthorizationManagers -There are also helpful static factories in `AuthenticationManagers` for composing individual ``AuthenticationManager``s into more sophisticated expressions. - [[authz-custom-authorization-manager]] ==== Custom Authorization Managers Obviously, you can also implement a custom `AuthorizationManager` and you can put just about any access-control logic you want in it. @@ -252,20 +198,12 @@ Java:: [source,java,role="primary"] ---- @Bean -static RoleHierarchy roleHierarchy() { - RoleHierarchyImpl hierarchy = new RoleHierarchyImpl(); +AccessDecisionVoter hierarchyVoter() { + RoleHierarchy hierarchy = new RoleHierarchyImpl(); hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF\n" + "ROLE_STAFF > ROLE_USER\n" + "ROLE_USER > ROLE_GUEST"); - return hierarchy; -} - -// and, if using method security also add -@Bean -static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) { - DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); - expressionHandler.setRoleHierarchy(roleHierarchy); - return expressionHandler; + return new RoleHierarchyVoter(hierarchy); } ---- @@ -273,6 +211,10 @@ Xml:: + [source,java,role="secondary"] ---- + + + + @@ -283,12 +225,6 @@ Xml:: - - - - - ---- ====== diff --git a/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc index 7dac6eb233..4c50685c54 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc @@ -1,1013 +1,11 @@ [[servlet-authorization-authorizationfilter]] -= Authorize HttpServletRequests += Authorize HttpServletRequests with AuthorizationFilter :figures: servlet/authorization -Spring Security allows you to xref:servlet/authorization/index.adoc[model your authorization] at the request level. -For example, with Spring Security you can say that all pages under `/admin` require one authority while all other pages simply require authentication. - -By default, Spring Security requires that every request be authenticated. -That said, any time you use xref:servlet/configuration/java.adoc#jc-httpsecurity[an `HttpSecurity` instance], it's necessary to declare your authorization rules. - -[[activate-request-security]] -Whenever you have an `HttpSecurity` instance, you should at least do: - -.Use authorizeHttpRequests -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -http - .authorizeHttpRequests((authorize) -> authorize - .anyRequest().authenticated() - ) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -http { - authorizeHttpRequests { - authorize(anyRequest, authenticated) - } -} ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - - ----- -====== - -This tells Spring Security that any endpoint in your application requires that the security context at a minimum be authenticated in order to allow it. - -In many cases, your authorization rules will be more sophisticated than that, so please consider the following use cases: - -* I have an app that uses `authorizeRequests` and I want to <> -* I want to <> -* I want to <> based on a pattern; specifically <> -* I want to match request, and I map Spring MVC to <> -* I want to <> -* I want to <> -* I want to <> -* I want to <> to a policy agent - -[[request-authorization-architecture]] -== Understanding How Request Authorization Components Work +This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works within Servlet-based applications. [NOTE] -This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works at the request level in Servlet-based applications. - -.Authorize HttpServletRequest -image::{figures}/authorizationfilter.png[] - -* image:{icondir}/number_1.png[] First, the `AuthorizationFilter` constructs a `Supplier` that retrieves an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder]. -* image:{icondir}/number_2.png[] Second, it passes the `Supplier` and the `HttpServletRequest` to the xref:servlet/architecture.adoc#authz-authorization-manager[`AuthorizationManager`]. -The `AuthorizationManager` matches the request to the patterns in `authorizeHttpRequests`, and runs the corresponding rule. -** image:{icondir}/number_3.png[] If authorization is denied, xref:servlet/authorization/events.adoc[an `AuthorizationDeniedEvent` is published], and an `AccessDeniedException` is thrown. -In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] handles the `AccessDeniedException`. -** image:{icondir}/number_4.png[] If access is granted, xref:servlet/authorization/events.adoc[an `AuthorizationGrantedEvent` is published] and `AuthorizationFilter` continues with the xref:servlet/architecture.adoc#servlet-filters-review[FilterChain] which allows the application to process normally. - -=== `AuthorizationFilter` Is Last By Default - -The `AuthorizationFilter` is last in xref:servlet/architecture.adoc#servlet-filterchain-figure[the Spring Security filter chain] by default. -This means that Spring Security's xref:servlet/authentication/index.adoc[authentication filters], xref:servlet/exploits/index.adoc[exploit protections], and other filter integrations do not require authorization. -If you add filters of your own before the `AuthorizationFilter`, they will also not require authorization; otherwise, they will. - -A place where this typically becomes important is when you are adding {spring-framework-reference-url}web.html#spring-web[Spring MVC] endpoints. -Because they are executed by the {spring-framework-reference-url}web.html#mvc-servlet[`DispatcherServlet`] and this comes after the `AuthorizationFilter`, your endpoints need to be <>. - -=== All Dispatches Are Authorized - -The `AuthorizationFilter` runs not just on every request, but on every dispatch. -This means that the `REQUEST` dispatch needs authorization, but also ``FORWARD``s, ``ERROR``s, and ``INCLUDE``s. - -For example, {spring-framework-reference-url}web.html#spring-web[Spring MVC] can `FORWARD` the request to a view resolver that renders a Thymeleaf template, like so: - -.Sample Forwarding Spring MVC Controller -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Controller -public class MyController { - @GetMapping("/endpoint") - public String endpoint() { - return "endpoint"; - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Controller -class MyController { - @GetMapping("/endpoint") - fun endpoint(): String { - return "endpoint" - } -} ----- -====== - -In this case, authorization happens twice; once for authorizing `/endpoint` and once for forwarding to Thymeleaf to render the "endpoint" template. - -For that reason, you may want to <>. - -Another example of this principle is {spring-boot-reference-url}web.html#web.servlet.spring-mvc.error-handling[how Spring Boot handles errors]. -If the container catches an exception, say like the following: - -.Sample Erroring Spring MVC Controller -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Controller -public class MyController { - @GetMapping("/endpoint") - public String endpoint() { - throw new UnsupportedOperationException("unsupported"); - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Controller -class MyController { - @GetMapping("/endpoint") - fun endpoint(): String { - throw UnsupportedOperationException("unsupported") - } -} ----- -====== - -then Boot will dispatch it to the `ERROR` dispatch. - -In that case, authorization also happens twice; once for authorizing `/endpoint` and once for dispatching the error. - -For that reason, you may want to <>. - -=== `Authentication` Lookup is Deferred - -Remember that xref:servlet/authorization/architecture.adoc#_the_authorizationmanager[the `AuthorizationManager` API uses a `Supplier`]. - -This matters with `authorizeHttpRequests` when requests are <>. -In those cases, xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] is not queried, making for a faster request. - -[[authorizing-endpoints]] -== Authorizing an Endpoint - -You can configure Spring Security to have different rules by adding more rules in order of precedence. - -If you want to require that `/endpoint` only be accessible by end users with the `USER` authority, then you can do: - -.Authorize an Endpoint -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -SecurityFilterChain web(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests((authorize) -> authorize - .requestMatchers("/endpoint").hasAuthority('USER') - .anyRequest().authenticated() - ) - // ... - - return http.build(); -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Bean -SecurityFilterChain web(HttpSecurity http) throws Exception { - http { - authorizeHttpRequests { - authorize("/endpoint", hasAuthority('USER')) - authorize(anyRequest, authenticated) - } - } - return http.build(); -} ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - - - ----- -====== - -As you can see, the declaration can be broken up in to pattern/rule pairs. - -`AuthorizationFilter` processes these pairs in the order listed, applying only the first match to the request. -This means that even though `/**` would also match for `/endpoint` the above rules are not a problem. -The way to read the above rules is "if the request is `/endpoint`, then require the `USER` authority; else, only require authentication". - -Spring Security supports several patterns and several rules; you can also programmatically create your own of each. - -Once authorized, you can test it using xref:servlet/test/method.adoc#test-method-withmockuser[Security's test support] in the following way: - -.Test Endpoint Authorization -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@WithMockUser(authorities="USER") -@Test -void endpointWhenUserAuthorityThenAuthorized() { - this.mvc.perform(get("/endpoint")) - .andExpect(status().isOk()); -} - -@WithMockUser -@Test -void endpointWhenNotUserAuthorityThenForbidden() { - this.mvc.perform(get("/endpoint")) - .andExpect(status().isForbidden()); -} - -@Test -void anyWhenUnauthenticatedThenUnauthorized() { - this.mvc.perform(get("/any")) - .andExpect(status().isUnauthorized()) -} ----- -====== - -[[match-requests]] -== Matching Requests - -Above you've already seen <>. - -The first you saw was the simplest, which is to match any request. - -The second is to match by a URI pattern. -Spring Security supports two languages for URI pattern-matching: <> (as seen above) and <>. - -[[match-by-ant]] -=== Matching Using Ant -Ant is the default language that Spring Security uses to match requests. - -You can use it to match a single endpoint or a directory, and you can even capture placeholders for later use. -You can also refine it to match a specific set of HTTP methods. - -Let's say that you instead of wanting to match the `/endpoint` endpoint, you want to match all endpoints under the `/resource` directory. -In that case, you can do something like the following: - -.Match with Ant -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -http - .authorizeHttpRequests((authorize) -> authorize - .requestMatchers("/resource/**").hasAuthority("USER") - .anyRequest().authenticated() - ) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -http { - authorizeHttpRequests { - authorize("/resource/**", hasAuthority("USER")) - authorize(anyRequest, authenticated) - } -} ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - - - ----- -====== - -The way to read this is "if the request is `/resource` or some subdirectory, require the `USER` authority; otherwise, only require authentication" - -You can also extract path values from the request, as seen below: - -.Authorize and Extract -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -http - .authorizeHttpRequests((authorize) -> authorize - .requestMatchers("/resource/{name}").access(new WebExpressionAuthorizationManager("#name == authentication.name")) - .anyRequest().authenticated() - ) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -http { - authorizeHttpRequests { - authorize("/resource/{name}", WebExpressionAuthorizationManager("#name == authentication.name")) - authorize(anyRequest, authenticated) - } -} ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - - - ----- -====== - -Once authorized, you can test it using xref:servlet/test/method.adoc#test-method-withmockuser[Security's test support] in the following way: - -.Test Directory Authorization -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@WithMockUser(authorities="USER") -@Test -void endpointWhenUserAuthorityThenAuthorized() { - this.mvc.perform(get("/endpoint/jon")) - .andExpect(status().isOk()); -} - -@WithMockUser -@Test -void endpointWhenNotUserAuthorityThenForbidden() { - this.mvc.perform(get("/endpoint/jon")) - .andExpect(status().isForbidden()); -} - -@Test -void anyWhenUnauthenticatedThenUnauthorized() { - this.mvc.perform(get("/any")) - .andExpect(status().isUnauthorized()) -} ----- -====== - -[NOTE] -Spring Security only matches paths. -If you want to match query parameters, you will need a custom request matcher. - -[[match-by-regex]] -=== Matching Using Regular Expressions -Spring Security supports matching requests against a regular expression. -This can come in handy if you want to apply more strict matching criteria than `**` on a subdirectory. - -For example, consider a path that contains the username and the rule that all usernames must be alphanumeric. -You can use {security-api-url}org/springframework/security/web/util/matcher/RegexRequestMatcher.html[`RegexRequestMatcher`] to respect this rule, like so: - -.Match with Regex -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -http - .authorizeHttpRequests((authorize) -> authorize - .requestMatchers(RegexRequestMatcher.regexMatcher("/resource/[A-Za-z0-9]+")).hasAuthority("USER") - .anyRequest().denyAll() - ) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -http { - authorizeHttpRequests { - authorize(RegexRequestMatcher.regexMatcher("/resource/[A-Za-z0-9]+"), hasAuthority("USER")) - authorize(anyRequest, denyAll) - } -} ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - - - ----- -====== - -[[match-by-httpmethod]] -=== Matching By Http Method - -You can also match rules by HTTP method. -One place where this is handy is when authorizing by permissions granted, like being granted a `read` or `write` privilege. - -To require all ``GET``s to have the `read` permission and all ``POST``s to have the `write` permission, you can do something like this: - -.Match by HTTP Method -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -http - .authorizeHttpRequests((authorize) -> authorize - .requestMatchers(HttpMethod.GET).hasAuthority("read") - .requestMatchers(HttpMethod.POST).hasAuthority("write") - .anyRequest().denyAll() - ) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -http { - authorizeHttpRequests { - authorize(HttpMethod.GET, hasAuthority("read")) - authorize(HttpMethod.POST, hasAuthority("write")) - authorize(anyRequest, denyAll) - } -} ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - - - - ----- -====== - -These authorization rules should read as: "if the request is a GET, then require `read` permission; else, if the request is a POST, then require `write` permission; else, deny the request" - -[TIP] -Denying the request by default is a healthy security practice since it turns the set of rules into an allow list. - -Once authorized, you can test it using xref:servlet/test/method.adoc#test-method-withmockuser[Security's test support] in the following way: - -.Test Http Method Authorization -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@WithMockUser(authorities="read") -@Test -void getWhenReadAuthorityThenAuthorized() { - this.mvc.perform(get("/any")) - .andExpect(status().isOk()); -} - -@WithMockUser -@Test -void getWhenNoReadAuthorityThenForbidden() { - this.mvc.perform(get("/any")) - .andExpect(status().isForbidden()); -} - -@WithMockUser(authorities="write") -@Test -void postWhenWriteAuthorityThenAuthorized() { - this.mvc.perform(post("/any").with(csrf())) - .andExpect(status().isOk()) -} - -@WithMockUser(authorities="read") -@Test -void postWhenNoWriteAuthorityThenForbidden() { - this.mvc.perform(get("/any").with(csrf())) - .andExpect(status().isForbidden()); -} ----- -====== - -[[match-by-dispatcher-type]] -=== Matching By Dispatcher Type - -[NOTE] -This feature is not currently supported in XML - -As stated earlier, Spring Security <<_all_dispatches_are_authorized, authorizes all dispatcher types by default>>. -And even though xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontext[the security context] established on the `REQUEST` dispatch carries over to subsequent dispatches, subtle mismatches can sometimes cause an unexpected `AccessDeniedException`. - -To address that, you can configure Spring Security Java configuration to allow dispatcher types like `FORWARD` and `ERROR`, like so: - -.Match by Dispatcher Type -==== -.Java -[source,java,role="secondary"] ----- -http - .authorizeHttpRequests((authorize) -> authorize - .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll() - .requestMatchers("/endpoint").permitAll() - .anyRequest().denyAll() - ) ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -http { - authorizeHttpRequests { - authorize(DispatcherTypeRequestMatcher(DispatcherType.FORWARD), permitAll) - authorize(DispatcherTypeRequestMatcher(DispatcherType.ERROR), permitAll) - authorize("/endpoint", permitAll) - authorize(anyRequest, denyAll) - } -} ----- -==== - -[[match-by-servlet-path]] -[[mvc-not-default-servlet]] -[[match-by-mvc]] -=== Matching by Servlet Pattern - -Generally speaking, you can use `requestMatchers(String...)` and `requestMatchers(HttpMethod, String...)` as demonstrated above. - -However, if you map Spring MVC to a different servlet path, then you need to account for that in your security configuration. - -For example, if Spring MVC is mapped to `/mvc` instead of `/` (the default), then you may have an endpoint like `/mvc/my/controller` that you want to authorize. - -If you have multiple servlets, and `DispatcherServlet` is mapped in this way, you'll see an error that's something like this: - -[source,bash] ----- -This method cannot decide whether these patterns are Spring MVC patterns or not - -... - -For your reference, here is your servlet configuration: {default=[/], dispatcherServlet=[/mvc/*]} - -To address this, you need to specify the servlet path or pattern for each endpoint. -You can use .forServletPattern in conjunction with requestMatchers do to this ----- - -You can use `.forServletPattern` (or construct your own `MvcRequestMatcher` instance) to split the servlet path and the controller path in your configuration, like so: - -.Match by MvcRequestMatcher -==== -.Java -[source,java,role="primary"] ----- -@Bean -SecurityFilterChain appEndpoints(HttpSecurity http, MvcRequestMatcher.Builder mvc) { - http - .authorizeHttpRequests((authorize) -> authorize - .forServletPattern("/mvc/*", (mvc) -> mvc - .requestMatchers("/my/resource/**").hasAuthority("resource:read") - .anyRequest().authenticated() - ) - ); - - return http.build(); -} ----- -==== - -where `/mvc/*` is the matching pattern in your servlet configuration listed in the error message. - -This need can arise in at least two different ways: - -* If you use the `spring.mvc.servlet.path` Boot property to change the default path (`/`) to something else -* If you register more than one Spring MVC `DispatcherServlet` (thus requiring that one of them not be the default servlet) - -Note that when either of these cases come up, all URIs need to be fully-qualified as above. - -For example, consider a more sophisticated setup where you have Spring MVC resources mapped to `/mvc/*` and Spring Boot H2 Console mapped to `/h2-console/*`. -In that case, each URI can be made absolute, listing the servlet path like so: - -.Match by Servlet Path -==== -.Java -[source,java,role="primary"] ----- -@Bean -SecurityFilterChain appSecurity(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests((authorize) -> authorize - .forServletPattern("/mvc/*", (mvc) -> mvc - .requestMatchers("/my/resource/**").hasAuthority("resource:read") - ) - .forServletPattern("/h2-console/*", (h2) -> h2 - .anyRequest().hasAuthority("h2") - ) - ) - // ... -} ----- -==== - -Alternatively, you can do one of three things to remove the need to disambiguate: - -1. Always deploy `DispatcherServlet` to `/` (the default behavior) -+ -When `DispatcherServlet` is mapped to `/`, it's clear that all the URIs supplied in `requestMatchers(String)` are absolute URIs. -Because of that, there is no ambiguity when interpreting them. -+ -2. Remove all other servlets -+ -When there is only `DispatcherServlet`, it's clear that all the URIs supplied in `requestMatchers(String)` are relative to the Spring MVC configuration. -Because of that, there is no ambiguity when interpreting them. - -At times, servlet containers add other servlets by default that you aren't actually using. -So, if these aren't needed, remove them, bringing you down to just `DispatcherServlet`. -+ -3. Create an `HttpRequestHandler` so that `DispatcherServlet` dispatches to your servlets instead of your servlet container. -+ -If you are deploying Spring MVC to a separate path to allow your container to serve static resources, consider instead {spring-framework-reference-url}web/webmvc/mvc-config/default-servlet-handler.html#page-title[notifying Spring MVC about this]. -Or, if you have a custom servlet, publishing {spring-framework-api-url}org/springframework/web/servlet/mvc/HttpRequestHandlerAdapter.html[a custom `HttpRequestHandler` bean within {spring-framework-api-url}org/springframework/web/servlet/DispatcherServlet.html[the `DispatcherServlet` configuration] instead. -+ - -=== Matching by the Default Servlet - -You can also match more generally by the matching pattern specified in your servlet configuration. - -For example, to match the default servlet (whichever servlet is mapped to `/`), use `forServletPattern` like so: - -.Match by the Default Servlet -==== -.Java -[source,java,role="primary"] ----- -@Bean -SecurityFilterChain appSecurity(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests((authorize) -> authorize - .forServletPattern("/", (root) -> root - .requestMatchers("/my/resource/**").hasAuthority("resource:read") - ) - ) - // ... -} ----- -==== - -Such will match on requests that the servlet container matches to your default servlet that start with the URI `/my/resource`. - -=== Matching by an Extension Servlet - -Or, to match to an extension servlet (like a servlet mapped to `*.jsp`), use `forServletPattern` as follows: - -.Match by an Extension Servlet -==== -.Java -[source,java,role="primary"] ----- -@Bean -SecurityFilterChain appSecurity(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests((authorize) -> authorize - .forServletPattern("*.jsp", (jsp) -> jsp - .requestMatchers("/my/resource/**").hasAuthority("resource:read") - ) - ) - // ... -} ----- -==== - -Such will match on requests that the servlet container matches to your `*.jsp` servlet that start with the URI `/my/resource` (for example a request like `/my/resource/page.jsp`). - -[[match-by-custom]] -=== Using a Custom Matcher - -[NOTE] -This feature is not currently supported in XML - -In Java configuration, you can create your own {security-api-url}org/springframework/security/web/util/matcher/RequestMatcher.html[`RequestMatcher`] and supply it to the DSL like so: - -.Authorize by Dispatcher Type -==== -.Java -[source,java,role="secondary"] ----- -RequestMatcher printview = (request) -> request.getParameter("print") != null; -http - .authorizeHttpRequests((authorize) -> authorize - .requestMatchers(printview).hasAuthority("print") - .anyRequest().authenticated() - ) ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val printview: RequestMatcher = { (request) -> request.getParameter("print") != null } -http { - authorizeHttpRequests { - authorize(printview, hasAuthority("print")) - authorize(anyRequest, authenticated) - } -} ----- -==== - -[TIP] -Because {security-api-url}org/springframework/security/web/util/matcher/RequestMatcher.html[`RequestMatcher`] is a functional interface, you can supply it as a lambda in the DSL. -However, if you want to extract values from the request, you will need to have a concrete class since that requires overriding a `default` method. - -Once authorized, you can test it using xref:servlet/test/method.adoc#test-method-withmockuser[Security's test support] in the following way: - -.Test Custom Authorization -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@WithMockUser(authorities="print") -@Test -void printWhenPrintAuthorityThenAuthorized() { - this.mvc.perform(get("/any?print")) - .andExpect(status().isOk()); -} - -@WithMockUser -@Test -void printWhenNoPrintAuthorityThenForbidden() { - this.mvc.perform(get("/any?print")) - .andExpect(status().isForbidden()); -} ----- -====== - -[[authorize-requests]] -== Authorizing Requests - -Once a request is matched, you can authorize it in several ways <> like `permitAll`, `denyAll`, and `hasAuthority`. - -As a quick summary, here are the authorization rules built into the DSL: - -* `permitAll` - The request requires no authorization and is a public endpoint; note that in this case, xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] is never retrieved from the session -* `denyAll` - The request is not allowed under any circumstances; note that in this case, the `Authentication` is never retrieved from the session -* `hasAuthority` - The request requires that the `Authentication` have xref:servlet/authorization/architecture.adoc#authz-authorities[a `GrantedAuthority`] that matches the given value -* `hasRole` - A shortcut for `hasAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix -* `hasAnyAuthority` - The request requires that the `Authentication` have a `GrantedAuthority` that matches any of the given values -* `hasAnyRole` - A shortcut for `hasAnyAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix -* `access` - The request uses this custom `AuthorizationManager` to determine access - -Having now learned the patterns, rules, and how they can be paired together, you should be able to understand what is going on in this more complex example: - -.Authorize Requests -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -import static jakarta.servlet.DispatcherType.*; - -import static org.springframework.security.authorization.AuthorizationManagers.allOf; -import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority; -import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole; - -@Bean -SecurityFilterChain web(HttpSecurity http) throws Exception { - http - // ... - .authorizeHttpRequests(authorize -> authorize // <1> - .dispatcherTypeMatchers(FORWARD, ERROR).permitAll() // <2> - .requestMatchers("/static/**", "/signup", "/about").permitAll() // <3> - .requestMatchers("/admin/**").hasRole("ADMIN") // <4> - .requestMatchers("/db/**").access(allOf(hasAuthority('db'), hasRole('ADMIN'))) // <5> - .anyRequest().denyAll() // <6> - ); - - return http.build(); -} ----- -====== -<1> There are multiple authorization rules specified. -Each rule is considered in the order they were declared. -<2> Dispatches `FORWARD` and `ERROR` are permitted to allow {spring-framework-reference-url}web.html#spring-web[Spring MVC] to render views and Spring Boot to render errors -<3> We specified multiple URL patterns that any user can access. -Specifically, any user can access a request if the URL starts with "/resources/", equals "/signup", or equals "/about". -<4> Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN". -You will notice that since we are invoking the `hasRole` method we do not need to specify the "ROLE_" prefix. -<5> Any URL that starts with "/db/" requires the user to have both been granted the "db" permission as well as be a "ROLE_ADMIN". -You will notice that since we are using the `hasRole` expression we do not need to specify the "ROLE_" prefix. -<6> Any URL that has not already been matched on is denied access. -This is a good strategy if you do not want to accidentally forget to update your authorization rules. - -[[authorization-expressions]] -== Expressing Authorization with SpEL - -While using a concrete `AuthorizationManager` is recommended, there are some cases where an expression is necessary, like with `` or with JSP Taglibs. -For that reason, this section will focus on examples from those domains. - -Given that, let's cover Spring Security's Web Security Authorization SpEL API a bit more in depth. - -Spring Security encapsulates all of its authorization fields and methods in a set of root objects. -The most generic root object is called `SecurityExpressionRoot` and it forms the basis for `WebSecurityExpressionRoot`. -Spring Security supplies this root object to `StandardEvaluationContext` when preparing to evaluate an authorization expression. - -[[using-authorization-expression-fields-and-methods]] -=== Using Authorization Expression Fields and Methods - -The first thing this provides is an enhanced set of authorization fields and methods to your SpEL expressions. -What follows is a quick overview of the most common methods: - -* `permitAll` - The request requires no authorization to be invoked; note that in this case, xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] is never retrieved from the session -* `denyAll` - The request is not allowed under any circumstances; note that in this case, the `Authentication` is never retrieved from the session -* `hasAuthority` - The request requires that the `Authentication` have xref:servlet/authorization/architecture.adoc#authz-authorities[a `GrantedAuthority`] that matches the given value -* `hasRole` - A shortcut for `hasAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix -* `hasAnyAuthority` - The request requires that the `Authentication` have a `GrantedAuthority` that matches any of the given values -* `hasAnyRole` - A shortcut for `hasAnyAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix -* `hasPermission` - A hook into your `PermissionEvaluator` instance for doing object-level authorization - -And here is a brief look at the most common fields: - -* `authentication` - The `Authentication` instance associated with this method invocation -* `principal` - The `Authentication#getPrincipal` associated with this method invocation - -Having now learned the patterns, rules, and how they can be paired together, you should be able to understand what is going on in this more complex example: - -.Authorize Requests Using SpEL -[tabs] -====== -Xml:: -+ -[source,java,role="primary"] ----- - - <1> - <2> - <3> - <4> - ----- -====== -<1> We specified a URL patters that any user can access. -Specifically, any user can access a request if the URL starts with "/static/". -<2> Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN". -You will notice that since we are invoking the `hasRole` method we do not need to specify the "ROLE_" prefix. -<3> Any URL that starts with "/db/" requires the user to have both been granted the "db" permission as well as be a "ROLE_ADMIN". -You will notice that since we are using the `hasRole` expression we do not need to specify the "ROLE_" prefix. -<4> Any URL that has not already been matched on is denied access. -This is a good strategy if you do not want to accidentally forget to update your authorization rules. - -[[using_path_parameters]] -=== Using Path Parameters - -Additionally, Spring Security provides a mechanism for discovering path parameters so they can also be accessed in the SpEL expression as well. - -For example, you can access a path parameter in your SpEL expression in the following way: - -.Authorize Request using SpEL path variable -[tabs] -====== -Xml:: -+ -[source,xml,role="primary"] ----- - - - - ----- -====== - -This expression refers to the path variable after `/resource/` and requires that it is equal to `Authentication#getName`. - -[[remote-authorization-manager]] -=== Use an Authorization Database, Policy Agent, or Other Service -If you want to configure Spring Security to use a separate service for authorization, you can create your own `AuthorizationManager` and match it to `anyRequest`. - -First, your `AuthorizationManager` may look something like this: - -.Open Policy Agent Authorization Manager -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Component -public final class OpenPolicyAgentAuthorizationManager implements AuthorizationManager { - @Override - public AuthorizationDecision check(Supplier authentication, RequestAuthorizationContext context) { - // make request to Open Policy Agent - } -} ----- -====== - -Then, you can wire it into Spring Security in the following way: - -.Any Request Goes to Remote Service -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -SecurityFilterChain web(HttpSecurity http, AuthorizationManager authz) throws Exception { - http - // ... - .authorizeHttpRequests((authorize) -> authorize - .anyRequest().access(authz) - ); - - return http.build(); -} ----- -====== - -[[favor-permitall]] -=== Favor `permitAll` over `ignoring` -When you have static resources it can be tempting to configure the filter chain to ignore these values. -A more secure approach is to permit them using `permitAll` like so: - -.Permit Static Resources -==== -.Java -[source,java,role="secondary"] ----- -http - .authorizeHttpRequests((authorize) -> authorize - .requestMatchers("/css/**").permitAll() - .anyRequest().authenticated() - ) ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -http { - authorizeHttpRequests { - authorize("/css/**", permitAll) - authorize(anyRequest, authenticated) - } -} ----- -==== - -It's more secure because even with static resources it's important to write secure headers, which Spring Security cannot do if the request is ignored. - -In this past, this came with a performance tradeoff since the session was consulted by Spring Security on every request. -As of Spring Security 6, however, the session is no longer pinged unless required by the authorization rule. -Because the performance impact is now addressed, Spring Security recommends using at least `permitAll` for all requests. - -[[migrate-authorize-requests]] -== Migrating from `authorizeRequests` - -[NOTE] -`AuthorizationFilter` supersedes {security-api-url}org/springframework/security/web/access/intercept/FilterSecurityInterceptor.html[`FilterSecurityInterceptor`]. +`AuthorizationFilter` supersedes xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`]. To remain backward compatible, `FilterSecurityInterceptor` remains the default. This section discusses how `AuthorizationFilter` works and how to override the default configuration. @@ -1015,7 +13,7 @@ The {security-api-url}org/springframework/security/web/access/intercept/Authoriz It is inserted into the xref:servlet/architecture.adoc#servlet-filterchainproxy[FilterChainProxy] as one of the xref:servlet/architecture.adoc#servlet-security-filters[Security Filters]. You can override the default when you declare a `SecurityFilterChain`. -Instead of using {security-api-url}org/springframework/security/config/annotation/web/builders/HttpSecurity.html#authorizeRequests()[`authorizeRequests`], use `authorizeHttpRequests`, like so: +Instead of using xref:servlet/authorization/authorize-http-requests.adoc#servlet-authorize-requests-defaults[`authorizeRequests`], use `authorizeHttpRequests`, like so: .Use authorizeHttpRequests [tabs] @@ -1045,62 +43,267 @@ This simplifies reuse and customization. Instead of the authentication needing to be looked up for every request, it will only look it up in requests where an authorization decision requires authentication. 3. Bean-based configuration support. -When `authorizeHttpRequests` is used instead of `authorizeRequests`, then {security-api-url}org/springframework/security/web/access/intercept/AuthorizationFilter.html[`AuthorizationFilter`] is used instead of {security-api-url}org/springframework/security/web/access/intercept/FilterSecurityInterceptor.html[`FilterSecurityInterceptor`]. +When `authorizeHttpRequests` is used instead of `authorizeRequests`, then {security-api-url}org/springframework/security/web/access/intercept/AuthorizationFilter.html[`AuthorizationFilter`] is used instead of xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`]. -=== Migrating Expressions +.Authorize HttpServletRequest +image::{figures}/authorizationfilter.png[] -Where possible, it is recommended that you use type-safe authorization managers instead of SpEL. -For Java configuration, {security-api-url}org/springframework/security/web/access/expression/WebExpressionAuthorizationManager.html[`WebExpressionAuthorizationManager`] is available to help migrate legacy SpEL. +* image:{icondir}/number_1.png[] First, the `AuthorizationFilter` obtains an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder]. +It wraps this in an `Supplier` in order to delay lookup. +* image:{icondir}/number_2.png[] Second, it passes the `Supplier` and the `HttpServletRequest` to the xref:servlet/architecture.adoc#authz-authorization-manager[`AuthorizationManager`]. +** image:{icondir}/number_3.png[] If authorization is denied, an `AccessDeniedException` is thrown. +In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] handles the `AccessDeniedException`. +** image:{icondir}/number_4.png[] If access is granted, `AuthorizationFilter` continues with the xref:servlet/architecture.adoc#servlet-filters-review[FilterChain] which allows the application to process normally. -To use `WebExpressionAuthorizationManager`, you can construct one with the expression you are trying to migrate, like so: +We can configure Spring Security to have different rules by adding more rules in order of precedence. +.Authorize Requests [tabs] ====== Java:: + [source,java,role="primary"] ---- -.requestMatchers("/test/**").access(new WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')")) ----- +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + // ... + .authorizeHttpRequests(authorize -> authorize // <1> + .requestMatchers("/resources/**", "/signup", "/about").permitAll() // <2> + .requestMatchers("/admin/**").hasRole("ADMIN") // <3> + .requestMatchers("/db/**").access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and hasRole('DBA')")) // <4> + // .requestMatchers("/db/**").access(AuthorizationManagers.allOf(AuthorityAuthorizationManager.hasRole("ADMIN"), AuthorityAuthorizationManager.hasRole("DBA"))) // <5> + .anyRequest().denyAll() // <6> + ); -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -.requestMatchers("/test/**").access(WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')")) + return http.build(); +} ---- ====== +<1> There are multiple authorization rules specified. +Each rule is considered in the order they were declared. +<2> We specified multiple URL patterns that any user can access. +Specifically, any user can access a request if the URL starts with "/resources/", equals "/signup", or equals "/about". +<3> Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN". +You will notice that since we are invoking the `hasRole` method we do not need to specify the "ROLE_" prefix. +<4> Any URL that starts with "/db/" requires the user to have both "ROLE_ADMIN" and "ROLE_DBA". +You will notice that since we are using the `hasRole` expression we do not need to specify the "ROLE_" prefix. +<5> The same rule from 4, could be written by combining multiple `AuthorizationManager`. +<6> Any URL that has not already been matched on is denied access. +This is a good strategy if you do not want to accidentally forget to update your authorization rules. -If you are referring to a bean in your expression like so: `@webSecurity.check(authentication, request)`, it's recommended that you instead call the bean directly, which will look something like the following: +You can take a bean-based approach by constructing your own xref:servlet/authorization/architecture.adoc#authz-delegate-authorization-manager[`RequestMatcherDelegatingAuthorizationManager`] like so: +.Configure RequestMatcherDelegatingAuthorizationManager [tabs] ====== Java:: + [source,java,role="primary"] ---- -.requestMatchers("/test/**").access((authentication, context) -> - new AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest()))) +@Bean +SecurityFilterChain web(HttpSecurity http, AuthorizationManager access) + throws AuthenticationException { + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().access(access) + ) + // ... + + return http.build(); +} + +@Bean +AuthorizationManager requestMatcherAuthorizationManager(HandlerMappingIntrospector introspector) { + MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector); + RequestMatcher permitAll = + new AndRequestMatcher( + mvcMatcherBuilder.pattern("/resources/**"), + mvcMatcherBuilder.pattern("/signup"), + mvcMatcherBuilder.pattern("/about")); + RequestMatcher admin = mvcMatcherBuilder.pattern("/admin/**"); + RequestMatcher db = mvcMatcherBuilder.pattern("/db/**"); + RequestMatcher any = AnyRequestMatcher.INSTANCE; + AuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .add(permitAll, (context) -> new AuthorizationDecision(true)) + .add(admin, AuthorityAuthorizationManager.hasRole("ADMIN")) + .add(db, AuthorityAuthorizationManager.hasRole("DBA")) + .add(any, new AuthenticatedAuthorizationManager()) + .build(); + return (context) -> manager.check(context.getRequest()); +} +---- +====== + +You can also wire xref:servlet/authorization/architecture.adoc#authz-custom-authorization-manager[your own custom authorization managers] for any request matcher. + +Here is an example of mapping a custom authorization manager to the `my/authorized/endpoint`: + +.Custom Authorization Manager +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .requestMatchers("/my/authorized/endpoint").access(new CustomAuthorizationManager()); + ) + // ... + + return http.build(); +} +---- +====== + +Or you can provide it for all requests as seen below: + +.Custom Authorization Manager for All Requests +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().access(new CustomAuthorizationManager()); + ) + // ... + + return http.build(); +} +---- +====== + +By default, the `AuthorizationFilter` applies to all dispatcher types. +We can configure Spring Security to not apply the authorization rules to all dispatcher types by using the `shouldFilterAllDispatcherTypes` method: + +.Set shouldFilterAllDispatcherTypes to false +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .shouldFilterAllDispatcherTypes(false) + .anyRequest().authenticated() + ) + // ... + + return http.build(); +} ---- Kotlin:: + [source,kotlin,role="secondary"] ---- -.requestMatchers("/test/**").access((authentication, context): AuthorizationManager -> - AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest()))) +@Bean +open fun web(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + shouldFilterAllDispatcherTypes = false + authorize(anyRequest, authenticated) + } + } + return http.build() +} ---- ====== -For complex instructions that include bean references as well as other expressions, it is recommended that you change those to implement `AuthorizationManager` and refer to them by calling `.access(AuthorizationManager)`. +Instead of setting `shouldFilterAllDispatcherTypes` to `false`, the recommended approach is to customize authorization on the dispatcher types. +For example, you may want to grant all access on requests with dispatcher type `ASYNC` or `FORWARD`. -If you are not able to do that, you can configure a {security-api-url}org/springframework/security/web/access/expression/DefaultHttpSecurityExpressionHandler.html[`DefaultHttpSecurityExpressionHandler`] with a bean resolver and supply that to `WebExpressionAuthorizationManager#setExpressionhandler`. +.Permit ASYNC and FORWARD dispatcher type +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .dispatcherTypeMatchers(DispatcherType.ASYNC, DispatcherType.FORWARD).permitAll() + .anyRequest().authenticated() + ) + // ... -[[security-matchers]] -== Security Matchers + return http.build(); +} +---- -The {security-api-url}org/springframework/security/web/util/matcher/RequestMatcher.html[`RequestMatcher`] interface is used to determine if a request matches a given rule. -We use `securityMatchers` to determine if xref:servlet/configuration/java.adoc#jc-httpsecurity[a given `HttpSecurity`] should be applied to a given request. +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +open fun web(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(DispatcherTypeRequestMatcher(DispatcherType.ASYNC, DispatcherType.FORWARD), permitAll) + authorize(anyRequest, authenticated) + } + } + return http.build() +} +---- +====== + +You can also customize it to require a specific role for a dispatcher type: + +.Require ADMIN for Dispatcher Type ERROR +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .dispatcherTypeMatchers(DispatcherType.ERROR).hasRole("ADMIN") + .anyRequest().authenticated() + ) + // ... + + return http.build(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +open fun web(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(DispatcherTypeRequestMatcher(DispatcherType.ERROR), hasRole("ADMIN")) + authorize(anyRequest, authenticated) + } + } + return http.build() +} +---- +====== + +== Request Matchers + +The `RequestMatcher` interface is used to determine if a request matches a given rule. +We use `securityMatchers` to determine if a given `HttpSecurity` should be applied to a given request. The same way, we can use `requestMatchers` to determine the authorization rules that we should apply to a given request. Look at the following example: @@ -1159,7 +362,7 @@ open class SecurityConfig { <3> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role <4> Any other request that doesn't match the rules above, will require authentication -The `securityMatcher(s)` and `requestMatcher(s)` methods will decide which `RequestMatcher` implementation fits best for your application: If {spring-framework-reference-url}web.html#spring-web[Spring MVC] is in the classpath, then {security-api-url}org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.html[`MvcRequestMatcher`] will be used, otherwise, {security-api-url}org/springframework/security/web/servlet/util/matcher/AntPathRequestMatcher.html[`AntPathRequestMatcher`] will be used. +The `securityMatcher(s)` and `requestMatcher(s)` methods will decide which `RequestMatcher` implementation fits best for your application: If Spring MVC is in the classpath, then `MvcRequestMatcher` will be used, otherwise, `AntPathRequestMatcher` will be used. You can read more about the Spring MVC integration xref:servlet/integrations/mvc.adoc[here]. If you want to use a specific `RequestMatcher`, just pass an implementation to the `securityMatcher` and/or `requestMatcher` methods: @@ -1236,7 +439,51 @@ open class SecurityConfig { <4> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role, using `RegexRequestMatcher` <5> Allow access to URLs that match the `MyCustomRequestMatcher` to users with the `SUPERVISOR` role, using a custom `RequestMatcher` -== Further Reading +== Expressions -Now that you have secured your application's requests, consider xref:servlet/authorization/method-security.adoc[securing its methods]. -You can also read further on xref:servlet/test/index.adoc[testing your application] or on integrating Spring Security with other aspects of you application like xref:servlet/integrations/data.adoc[the data layer] or xref:servlet/integrations/observability.adoc[tracing and metrics]. +It is recommended that you use type-safe authorization managers instead of SpEL. +However, `WebExpressionAuthorizationManager` is available to help migrate legacy SpEL. + +To use `WebExpressionAuthorizationManager`, you can construct one with the expression you are trying to migrate, like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +.requestMatchers("/test/**").access(new WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')")) +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +.requestMatchers("/test/**").access(WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')")) +---- +====== + +If you are referring to a bean in your expression like so: `@webSecurity.check(authentication, request)`, it's recommended that you instead call the bean directly, which will look something like the following: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +.requestMatchers("/test/**").access((authentication, context) -> + new AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest()))) +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +.requestMatchers("/test/**").access((authentication, context): AuthorizationManager -> + AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest()))) +---- +====== + +For complex instructions that include bean references as well as other expressions, it is recommended that you change those to implement `AuthorizationManager` and refer to them by calling `.access(AuthorizationManager)`. + +If you are not able to do that, you can configure a `DefaultHttpSecurityExpressionHandler` with a bean resolver and supply that to `WebExpressionAuthorizationManager#setExpressionhandler`. diff --git a/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc b/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc new file mode 100644 index 0000000000..62c44ac82b --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc @@ -0,0 +1,194 @@ +[[servlet-authorization-filtersecurityinterceptor]] += Authorize HttpServletRequest with FilterSecurityInterceptor +:figures: servlet/authorization + +[NOTE] +==== +`FilterSecurityInterceptor` is in the process of being replaced by xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`]. +Consider using that instead. +==== + +This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works within Servlet-based applications. + +The {security-api-url}org/springframework/security/web/access/intercept/FilterSecurityInterceptor.html[`FilterSecurityInterceptor`] provides xref:servlet/authorization/index.adoc#servlet-authorization[authorization] for `HttpServletRequest` instances. +It is inserted into the xref:servlet/architecture.adoc#servlet-filterchainproxy[FilterChainProxy] as one of the xref:servlet/architecture.adoc#servlet-security-filters[Security Filters]. + +The following image shows the role of `FilterSecurityInterceptor`: + +.Authorize HttpServletRequest +image::{figures}/filtersecurityinterceptor.png[] + +image:{icondir}/number_1.png[] The `FilterSecurityInterceptor` obtains an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder]. +image:{icondir}/number_2.png[] `FilterSecurityInterceptor` creates a {security-api-url}org/springframework/security/web/FilterInvocation.html[`FilterInvocation`] from the `HttpServletRequest`, `HttpServletResponse`, and `FilterChain` that are passed into the `FilterSecurityInterceptor`. +image:{icondir}/number_3.png[] It passes the `FilterInvocation` to `SecurityMetadataSource` to get the ``ConfigAttribute``s. +image:{icondir}/number_4.png[] It passes the `Authentication`, `FilterInvocation`, and ``ConfigAttribute``s to the `AccessDecisionManager`. +image:{icondir}/number_5.png[] If authorization is denied, an `AccessDeniedException` is thrown. +In this case, the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] handles the `AccessDeniedException`. +image:{icondir}/number_6.png[] If access is granted, `FilterSecurityInterceptor` continues with the xref:servlet/architecture.adoc#servlet-filters-review[`FilterChain`], which lets the application process normally. + +// configuration (xml/java) + +By default, Spring Security's authorization requires all requests to be authenticated. +The following listing shows the explicit configuration: + +[[servlet-authorize-requests-defaults]] +.Every Request Must be Authenticated +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // ... + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ); + return http.build(); +} +---- + +XML:: ++ +[source,xml,role="secondary"] +---- + + + + +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + // ... + authorizeRequests { + authorize(anyRequest, authenticated) + } + } + return http.build() +} +---- +====== + +We can configure Spring Security to have different rules by adding more rules in order of precedence: + +.Authorize Requests +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // ... + .authorizeRequests(authorize -> authorize // <1> + .requestMatchers("/resources/**", "/signup", "/about").permitAll() // <2> + .requestMatchers("/admin/**").hasRole("ADMIN") // <3> + .requestMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // <4> + .anyRequest().denyAll() // <5> + ); + return http.build(); +} +---- + +XML:: ++ +[source,xml,role="secondary"] +---- + + + + + + + + + + + +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeRequests { // <1> + authorize("/resources/**", permitAll) // <2> + authorize("/signup", permitAll) + authorize("/about", permitAll) + + authorize("/admin/**", hasRole("ADMIN")) // <3> + authorize("/db/**", "hasRole('ADMIN') and hasRole('DBA')") // <4> + authorize(anyRequest, denyAll) // <5> + } + } + return http.build() +} +---- +====== +<1> There are multiple authorization rules specified. +Each rule is considered in the order they were declared. +<2> We specified multiple URL patterns that any user can access. +Specifically, any user can access a request if the URL starts with "/resources/", equals "/signup", or equals "/about". +<3> Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN". +You will notice that since we are invoking the `hasRole` method we do not need to specify the "ROLE_" prefix. +<4> Any URL that starts with "/db/" requires the user to have both "ROLE_ADMIN" and "ROLE_DBA". +You will notice that since we are using the `hasRole` expression we do not need to specify the "ROLE_" prefix. +<5> Any URL that has not already been matched on is denied access. +This is a good strategy if you do not want to accidentally forget to update your authorization rules. + + +[[filtersecurityinterceptor-every-request]] +== Configure FilterSecurityInterceptor with Dispatcher Types + +By default, the `FilterSecurityInterceptor` applies to every request. +This means that if a request is dispatched from a request that was already filtered, the `FilterSecurityInterceptor` will perform the same authorization checks on the dispatched request. +In some scenarios, you may not want to apply authorization on some dispatcher types: + +.Permit ASYNC and ERROR dispatcher types +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .authorizeRequests((authorize) -> authorize + .dispatcherTypeMatchers(DispatcherType.ASYNC, DispatcherType.ERROR).permitAll() + .anyRequest.authenticated() + ) + // ... + + return http.build(); +} +---- + +XML:: ++ +[source,xml,role="secondary"] +---- + + + + + + + + + +---- +====== diff --git a/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc b/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc new file mode 100644 index 0000000000..24936c49bd --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc @@ -0,0 +1,550 @@ + +[[el-access]] += Expression-Based Access Control +Spring Security 3.0 introduced the ability to use Spring Expression Language (SpEL) expressions as an authorization mechanism in addition to the existing configuration attributes and access-decision voters. +Expression-based access control is built on the same architecture but lets complicated Boolean logic be encapsulated in a single expression. + + +== Overview +Spring Security uses SpEL for expression support and you should look at how that works if you are interested in understanding the topic in more depth. +Expressions are evaluated with a "`root object`" as part of the evaluation context. +Spring Security uses specific classes for web and method security as the root object to provide built-in expressions and access to values, such as the current principal. + +[[el-common-built-in]] +=== Common Built-In Expressions +The base class for expression root objects is `SecurityExpressionRoot`. +This provides some common expressions that are available in both web and method security: + +[[common-expressions]] +.Common built-in expressions +|=== +| Expression | Description + +| `hasRole(String role)` +| Returns `true` if the current principal has the specified role. + +Example: `hasRole('admin')` + +By default, if the supplied role does not start with `ROLE_`, it is added. +You can customize this behavior by modifying the `defaultRolePrefix` on `DefaultWebSecurityExpressionHandler`. + +| `hasAnyRole(String... roles)` +| Returns `true` if the current principal has any of the supplied roles (given as a comma-separated list of strings). + +Example: `hasAnyRole('admin', 'user')`. + +By default, if the supplied role does not start with `ROLE_`, it is added. +You can customize this behavior by modifying the `defaultRolePrefix` on `DefaultWebSecurityExpressionHandler`. + +| `hasAuthority(String authority)` +| Returns `true` if the current principal has the specified authority. + +Example: `hasAuthority('read')` + +| `hasAnyAuthority(String... authorities)` +| Returns `true` if the current principal has any of the supplied authorities (given as a comma-separated list of strings). + +Example: `hasAnyAuthority('read', 'write')`. + +| `principal` +| Allows direct access to the principal object that represents the current user. + +| `authentication` +| Allows direct access to the current `Authentication` object obtained from the `SecurityContext`. + +| `permitAll` +| Always evaluates to `true`. + +| `denyAll` +| Always evaluates to `false`. + +| `isAnonymous()` +| Returns `true` if the current principal is an anonymous user. + +| `isRememberMe()` +| Returns `true` if the current principal is a remember-me user. + +| `isAuthenticated()` +| Returns `true` if the user is not anonymous. + +| `isFullyAuthenticated()` +| Returns `true` if the user is not an anonymous and is not a remember-me user. + +| `hasPermission(Object target, Object permission)` +| Returns `true` if the user has access to the provided target for the given permission. +Example, `hasPermission(domainObject, 'read')`. + +| `hasPermission(Object targetId, String targetType, Object permission)` +| Returns `true` if the user has access to the provided target for the given permission. +Example, `hasPermission(1, 'com.example.domain.Message', 'read')`. +|=== + + + +[[el-access-web]] +== Web Security Expressions +To use expressions to secure individual URLs, you first need to set the `use-expressions` attribute in the `` element to `true`. +Spring Security then expects the `access` attributes of the `` elements to contain SpEL expressions. +Each expression should evaluate to a Boolean, defining whether access should be allowed or not. +The following listing shows an example: + +[source,xml] +---- + + + ... + +---- + +Here, we have defined that the `admin` area of an application (defined by the URL pattern) should be available only to users who have the granted authority (`admin`) and whose IP address matches a local subnet. +We have already seen the built-in `hasRole` expression in the previous section. +The `hasIpAddress` expression is an additional built-in expression that is specific to web security. +It is defined by the `WebSecurityExpressionRoot` class, an instance of which is used as the expression root object when evaluating web-access expressions. +This object also directly exposed the `HttpServletRequest` object under the name `request` so that you can invoke the request directly in an expression. +If expressions are being used, a `WebExpressionVoter` is added to the `AccessDecisionManager` that is used by the namespace. +So, if you do not use the namespace and want to use expressions, you have to add one of these to your configuration. + +[[el-access-web-beans]] +=== Referring to Beans in Web Security Expressions + +If you wish to extend the expressions that are available, you can easily refer to any Spring Bean you expose. +For example, you could use the following, assuming you have a Bean with the name of `webSecurity` that contains the following method signature: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +public class WebSecurity { + public boolean check(Authentication authentication, HttpServletRequest request) { + ... + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +class WebSecurity { + fun check(authentication: Authentication?, request: HttpServletRequest?): Boolean { + // ... + } +} +---- +====== + +You could then refer to the method as follows: + +.Refer to method +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/user/**").access(new WebExpressionAuthorizationManager("@webSecurity.check(authentication,request)")) + ... + ) +---- + +XML:: ++ +[source,xml,role="secondary"] +---- + + + ... + +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +http { + authorizeRequests { + authorize("/user/**", "@webSecurity.check(authentication,request)") + } +} +---- +====== + +[[el-access-web-path-variables]] +=== Path Variables in Web Security Expressions + +At times, it is nice to be able to refer to path variables within a URL. +For example, consider a RESTful application that looks up a user by ID from a URL path in a format of `+/user/{userId}+`. + +You can easily refer to the path variable by placing it in the pattern. +For example, you could use the following if you had a Bean with the name of `webSecurity` that contains the following method signature: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +public class WebSecurity { + public boolean checkUserId(Authentication authentication, int id) { + ... + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +class WebSecurity { + fun checkUserId(authentication: Authentication?, id: Int): Boolean { + // ... + } +} +---- +====== + +You could then refer to the method as follows: + +.Path Variables +[tabs] +====== +Java:: ++ +[source,java,role="primary",attrs="-attributes"] +---- +http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/user/{userId}/**").access(new WebExpressionAuthorizationManager("@webSecurity.checkUserId(authentication,#userId)")) + ... + ); +---- + +XML:: ++ +[source,xml,role="secondary",attrs="-attributes"] +---- + + + ... + +---- + +Kotlin:: ++ +[source,kotlin,role="secondary",attrs="-attributes"] +---- +http { + authorizeRequests { + authorize("/user/{userId}/**", "@webSecurity.checkUserId(authentication,#userId)") + } +} +---- +====== + +In this configuration, URLs that match would pass in the path variable (and convert it) into the `checkUserId` method. +For example, if the URL were `/user/123/resource`, the ID passed in would be `123`. + +== Method Security Expressions +Method security is a bit more complicated than a simple allow or deny rule. +Spring Security 3.0 introduced some new annotations to allow comprehensive support for the use of expressions. + +[[el-pre-post-annotations]] +=== @Pre and @Post Annotations +There are four annotations that support expression attributes to allow pre and post-invocation authorization checks and also to support filtering of submitted collection arguments or return values. +They are `@PreAuthorize`, `@PreFilter`, `@PostAuthorize`, and `@PostFilter`. +Their use is enabled through the `global-method-security` namespace element: + +[source,xml] +---- + +---- + +==== Access Control using @PreAuthorize and @PostAuthorize +The most obviously useful annotation is `@PreAuthorize`, which decides whether a method can actually be invoked or not. +The following example (from the {gh-samples-url}/servlet/xml/java/contacts["Contacts" sample application]) uses the `@PreAuthorize` annotation: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@PreAuthorize("hasRole('USER')") +public void create(Contact contact); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@PreAuthorize("hasRole('USER')") +fun create(contact: Contact?) +---- +====== + +This means that access is allowed only for users with the `ROLE_USER` role. +Obviously, the same thing could easily be achieved by using a traditional configuration and a simple configuration attribute for the required role. +However, consider the following example: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@PreAuthorize("hasPermission(#contact, 'admin')") +public void deletePermission(Contact contact, Sid recipient, Permission permission); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@PreAuthorize("hasPermission(#contact, 'admin')") +fun deletePermission(contact: Contact?, recipient: Sid?, permission: Permission?) +---- +====== + +Here, we actually use a method argument as part of the expression to decide whether the current user has the `admin` permission for the given contact. +The built-in `hasPermission()` expression is linked into the Spring Security ACL module through the application context, as we <>. +You can access any of the method arguments by name as expression variables. + +Spring Security can resolve the method arguments in a number of ways. +Spring Security uses `DefaultSecurityParameterNameDiscoverer` to discover the parameter names. +By default, the following options are tried for a method. + +* If Spring Security's `@P` annotation is present on a single argument to the method, the value is used. +This is useful for interfaces compiled with a JDK prior to JDK 8 (which do not contain any information about the parameter names). +The following example uses the `@P` annotation: + ++ + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +import org.springframework.security.access.method.P; + +... + +@PreAuthorize("#c.name == authentication.name") +public void doSomething(@P("c") Contact contact); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +import org.springframework.security.access.method.P + +... + +@PreAuthorize("#c.name == authentication.name") +fun doSomething(@P("c") contact: Contact?) +---- +====== + ++ + +Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation. + +* If Spring Data's `@Param` annotation is present on at least one parameter for the method, the value is used. +This is useful for interfaces compiled with a JDK prior to JDK 8 which do not contain any information about the parameter names. +The following example uses the `@Param` annotation: ++ +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +import org.springframework.data.repository.query.Param; + +... + +@PreAuthorize("#n == authentication.name") +Contact findContactByName(@Param("n") String name); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +import org.springframework.data.repository.query.Param + +... + +@PreAuthorize("#n == authentication.name") +fun findContactByName(@Param("n") name: String?): Contact? +---- +====== ++ + +Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation. + +* If JDK 8 was used to compile the source with the `-parameters` argument and Spring 4+ is being used, the standard JDK reflection API is used to discover the parameter names. +This works on both classes and interfaces. + +* Finally, if the code was compiled with the debug symbols, the parameter names are discovered by using the debug symbols. +This does not work for interfaces, since they do not have debug information about the parameter names. +For interfaces, annotations or the JDK 8 approach must be used. + +.[[el-pre-post-annotations-spel]] +Any SpEL functionality is available within the expression, so you can also access properties on the arguments. +For example, if you wanted a particular method to allow access only to a user whose username matched that of the contact, you could write + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@PreAuthorize("#contact.name == authentication.name") +public void doSomething(Contact contact); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@PreAuthorize("#contact.name == authentication.name") +fun doSomething(contact: Contact?) +---- +====== + +.[[el-pre-post-annotations-post]] +Here, we access another built-in expression, `authentication`, which is the `Authentication` stored in the security context. +You can also access its `principal` property directly, by using the `principal` expression. +The value is often a `UserDetails` instance, so you might use an expression such as `principal.username` or `principal.enabled`. + +==== Filtering using @PreFilter and @PostFilter +Spring Security supports filtering of collections, arrays, maps, and streams by using expressions. +This is most commonly performed on the return value of a method. +The following example uses `@PostFilter`: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@PreAuthorize("hasRole('USER')") +@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')") +public List getAll(); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@PreAuthorize("hasRole('USER')") +@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')") +fun getAll(): List +---- +====== + +When using the `@PostFilter` annotation, Spring Security iterates through the returned collection or map and removes any elements for which the supplied expression is false. +For an array, a new array instance that contains filtered elements is returned. +`filterObject` refers to the current object in the collection. +When a map is used, it refers to the current `Map.Entry` object, which lets you use `filterObject.key` or `filterObject.value` in the expression. +You can also filter before the method call by using `@PreFilter`, though this is a less common requirement. +The syntax is the same. However, if there is more than one argument that is a collection type, you have to select one by name using the `filterTarget` property of this annotation. + +Note that filtering is obviously not a substitute for tuning your data retrieval queries. +If you are filtering large collections and removing many of the entries, this is likely to be inefficient. + + +[[el-method-built-in]] +=== Built-In Expressions +There are some built-in expressions that are specific to method security, which we have already seen in use earlier. +The `filterTarget` and `returnValue` values are simple enough, but the use of the `hasPermission()` expression warrants a closer look. + + +[[el-permission-evaluator]] +==== The PermissionEvaluator interface +`hasPermission()` expressions are delegated to an instance of `PermissionEvaluator`. +It is intended to bridge between the expression system and Spring Security's ACL system, letting you specify authorization constraints on domain objects, based on abstract permissions. +It has no explicit dependencies on the ACL module, so you could swap that out for an alternative implementation if required. +The interface has two methods: + +[source,java] +---- +boolean hasPermission(Authentication authentication, Object targetDomainObject, + Object permission); + +boolean hasPermission(Authentication authentication, Serializable targetId, + String targetType, Object permission); +---- + +These methods map directly to the available versions of the expression, with the exception that the first argument (the `Authentication` object) is not supplied. +The first is used in situations where the domain object, to which access is being controlled, is already loaded. +Then the expression returns `true` if the current user has the given permission for that object. +The second version is used in cases where the object is not loaded but its identifier is known. +An abstract "`type`" specifier for the domain object is also required, letting the correct ACL permissions be loaded. +This has traditionally been the Java class of the object but does not have to be, as long as it is consistent with how the permissions are loaded. + +To use `hasPermission()` expressions, you have to explicitly configure a `PermissionEvaluator` in your application context. +The following example shows how to do so: + +[source,xml] +---- + + + + + + + +---- + +Where `myPermissionEvaluator` is the bean which implements `PermissionEvaluator`. +Usually, this is the implementation from the ACL module, which is called `AclPermissionEvaluator`. +See the {gh-samples-url}/servlet/xml/java/contacts[`Contacts`] sample application configuration for more details. + +==== Method Security Meta Annotations + +You can make use of meta annotations for method security to make your code more readable. +This is especially convenient if you find that you repeat the same complex expression throughout your code base. +For example, consider the following: + +[source,java] +---- +@PreAuthorize("#contact.name == authentication.name") +---- + +Instead of repeating this everywhere, you can create a meta annotation: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("#contact.name == authentication.name") +public @interface ContactPermission {} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Retention(AnnotationRetention.RUNTIME) +@PreAuthorize("#contact.name == authentication.name") +annotation class ContactPermission +---- +====== + +You can use meta annotations for any of the Spring Security method security annotations. +To remain compliant with the specification, JSR-250 annotations do not support meta annotations. + diff --git a/docs/modules/ROOT/pages/servlet/authorization/index.adoc b/docs/modules/ROOT/pages/servlet/authorization/index.adoc index 1a0579336b..a528115372 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/index.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/index.adoc @@ -2,12 +2,11 @@ = Authorization :page-section-summary-toc: 1 -Having established xref:servlet/authentication/index.adoc[how users will authenticate], you also need to configure your application's authorization rules. - The advanced authorization capabilities within Spring Security represent one of the most compelling reasons for its popularity. Irrespective of how you choose to authenticate (whether using a Spring Security-provided mechanism and provider or integrating with a container or other non-Spring Security authentication authority), the authorization services can be used within your application in a consistent and simple way. -You should consider attaching authorization rules to xref:servlet/authorization/authorize-http-requests.adoc[request URIs] and xref:servlet/authorization/method-security.adoc[methods] to begin. -Below there is also wealth of detail about xref:servlet/authorization/architecture.adoc[how Spring Security authorization works] and how, having established a basic model, it can be fine-tuned. +In this part, we explore the different `AbstractSecurityInterceptor` implementations, which were introduced in Part I. +We then move on to explore how to fine-tune authorization through the use of domain access control lists. + diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc index 83e840d326..c7ce9f02c3 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc @@ -1,27 +1,57 @@ [[jc-method]] = Method Security -:figures: servlet/authorization -In addition to xref:servlet/authorization/authorize-http-requests.adoc[modeling authorization at the request level], Spring Security also supports modeling at the method level. +From version 2.0 onwards, Spring Security has improved support substantially for adding security to your service layer methods. +It provides support for JSR-250 annotation security as well as the framework's original `@Secured` annotation. +From 3.0, you can also make use of new xref:servlet/authorization/expression-based.adoc#el-access[expression-based annotations]. +You can apply security to a single bean, by using the `intercept-methods` element to decorate the bean declaration, or you can secure multiple beans across the entire service layer by using AspectJ style pointcuts. -[[activate-method-security]] -You can activate it in your application by annotating any `@Configuration` class with `@EnableMethodSecurity` or adding `` to any XML configuration file, like so: +[[jc-enable-method-security]] +== EnableMethodSecurity +In Spring Security 5.6, we can enable annotation-based security using the `@EnableMethodSecurity` annotation on any `@Configuration` instance. + +This improves upon `@EnableGlobalMethodSecurity` in a number of ways. `@EnableMethodSecurity`: + +1. Uses the simplified `AuthorizationManager` API instead of metadata sources, config attributes, decision managers, and voters. +This simplifies reuse and customization. +2. Favors direct bean-based configuration, instead of requiring extending `GlobalMethodSecurityConfiguration` to customize beans +3. Is built using native Spring AOP, removing abstractions and allowing you to use Spring AOP building blocks to customize +4. Checks for conflicting annotations to ensure an unambiguous security configuration +5. Complies with JSR-250 +6. Enables `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` by default + +[NOTE] +==== +For earlier versions, please read about similar support with <>. +==== + +For example, the following would enable Spring Security's `@PreAuthorize` annotation: + +.Method Security Configuration [tabs] ====== Java:: + [source,java,role="primary"] ---- +@Configuration @EnableMethodSecurity +public class MethodSecurityConfig { + // ... +} ---- Kotlin:: + [source,kotlin,role="secondary"] ---- +@Configuration @EnableMethodSecurity +class MethodSecurityConfig { + // ... +} ---- Xml:: @@ -32,70 +62,26 @@ Xml:: ---- ====== -Then, you are immediately able to annotate any Spring-managed class or method with <>, <>, <>, and <> to authorize method invocations, including the input parameters and return values. - -[NOTE] -{spring-boot-reference-url}using.html#using.build-systems.starters[Spring Boot Starter Security] does not activate method-level authorization by default. - -Method Security supports many other use cases as well including <>, <>, and several configuration points. -Consider learning about the following use cases: - -* <> -* Understanding <> and reasons to use it -* Comparing <> -* Authorizing methods with <> and <> -* Filtering methods with <> and <> -* Authorizing methods with <> -* Authorizing methods with <> -* Integrating with <> -* Coordinating with <> -* Customizing <> -* Integrating with <> - -[[method-security-architecture]] -== How Method Security Works - -Spring Security's method authorization support is handy for: - -* Extracting fine-grained authorization logic; for example, when the method parameters and return values contribute to the authorization decision. -* Enforcing security at the service layer -* Stylistically favoring annotation-based over `HttpSecurity`-based configuration - -And since Method Security is built using {spring-framework-reference-url}core.html#aop-api[Spring AOP], you have access to all its expressive power to override Spring Security's defaults as needed. - -As already mentioned, you begin by adding `@EnableMethodSecurity` to a `@Configuration` class or `` in a Spring XML configuration file. - -[[use-method-security]] -[NOTE] -==== -This annotation and XML element supercede `@EnableGlobalMethodSecurity` and ``, respectively. -They offer the following improvements: - -1. Uses the simplified `AuthorizationManager` API instead of metadata sources, config attributes, decision managers, and voters. -This simplifies reuse and customization. -2. Favors direct bean-based configuration, instead of requiring extending `GlobalMethodSecurityConfiguration` to customize beans -3. Is built using native Spring AOP, removing abstractions and allowing you to use Spring AOP building blocks to customize -4. Checks for conflicting annotations to ensure an unambiguous security configuration -5. Complies with JSR-250 -6. Enables `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` by default - -If you are using `@EnableGlobalMethodSecurity` or ``, these are now deprecated, and you are encouraged to migrate. -==== - -Method authorization is a combination of before- and after-method authorization. -Consider a service bean that is annotated in the following way: +Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly. +Spring Security's native annotation support defines a set of attributes for the method. +These will be passed to the `DefaultAuthorizationMethodInterceptorChain` for it to make the actual decision: +.Method Security Annotation Usage [tabs] ====== Java:: + [source,java,role="primary"] ---- -@Service -public class MyCustomerService { - @PreAuthorize("hasAuthority('permission:read')") - @PostAuthorize("returnObject.owner == authentication.name") - public Customer readCustomer(String id) { ... } +public interface BankService { + @PreAuthorize("hasRole('USER')") + Account readAccount(Long id); + + @PreAuthorize("hasRole('USER')") + List findAccounts(); + + @PreAuthorize("hasRole('TELLER')") + Account post(Account account, Double amount); } ---- @@ -103,622 +89,44 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -@Service -open class MyCustomerService { - @PreAuthorize("hasAuthority('permission:read')") - @PostAuthorize("returnObject.owner == authentication.name") - fun readCustomer(val id: String): Customer { ... } +interface BankService { + @PreAuthorize("hasRole('USER')") + fun readAccount(id : Long) : Account + + @PreAuthorize("hasRole('USER')") + fun findAccounts() : List + + @PreAuthorize("hasRole('TELLER')") + fun post(account : Account, amount : Double) : Account } ---- ====== -A given invocation to `MyCustomerService#readCustomer` may look something like this when Method Security <>: - -image::{figures}/methodsecurity.png[] - -1. Spring AOP invokes its proxy method for `readCustomer`. Among the proxy's other advisors, it invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor/html[`AuthorizationManagerBeforeMethodInterceptor`] that matches <> -2. The interceptor invokes {security-api-url}org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.html[`PreAuthorizeAuthorizationManager#check`] -3. The authorization manager uses a `MethodSecurityExpressionHandler` to parse the annotation's <> and constructs a corresponding `EvaluationContext` from a `MethodSecurityExpressionRoot` containing xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[a `Supplier`] and `MethodInvocation`. -4. The interceptor uses this context to evaluate the expression; specifically, it reads xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] from the `Supplier` and checks whether it has `permission:read` in its collection of xref:servlet/authorization/architecture.adoc#authz-authorities[authorities] -5. If the evaluation passes, then Spring AOP proceeds to invoke the method. -6. If not, the interceptor publishes an `AuthorizationDeniedEvent` and throws an {security-api-url}org/springframework/security/access/AccessDeniedException.html[`AccessDeniedException`] which xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[the `ExceptionTranslationFilter`] catches and returns a 403 status code to the response -7. After the method returns, Spring AOP invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.html[`AuthorizationManagerAfterMethodInterceptor`] that matches <>, operating the same as above, but with {security-api-url}org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.html[`PostAuthorizeAuthorizationManager`] -8. If the evaluation passes (in this case, the return value belongs to the logged-in user), processing continues normally -9. If not, the interceptor publishes an `AuthorizationDeniedEvent` and throws an {security-api-url}org/springframework/security/access/AccessDeniedException.html[`AccessDeniedException`], which xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[the `ExceptionTranslationFilter`] catches and returns a 403 status code to the response - -[NOTE] -If the method is not being called in the context of an HTTP request, you will likely need to handle the `AccessDeniedException` yourself - -[[unanimous-based-authorization-decisions]] -=== Multiple Annotations Are Computed In Series - -As demonstrated above, if a method invocation involves multiple <>, each of those is processed one at a time. -This means that they can collectively be thought of as being "anded" together. -In other words, for an invocation to be authorized, all annotation inspections need to pass authorization. - -[[repeated-annotations]] -=== Repeated Annotations Are Not Supported - -That said, it is not supported to repeat the same annotation on the same method. -For example, you cannot please `@PreAuthorize` twice on the same method. - -Instead, use SpEL's boolean support or its support for delegating to a separate bean. - -[[annotation-method-pointcuts]] -=== Each Annotation Has Its Own Pointcut - -Each annotation has its own pointcut instance that looks for that annotation or its <> counterparts across the entire object hierarchy, starting at <>. - -You can see the specifics of this in {security-api-url}org/springframework/security/authorization/method/AuthorizationMethodPointcuts.html[`AuthorizationMethodPointcuts`]. - -[[annotation-method-interceptors]] -=== Each Annotation Has Its Own Method Interceptor - -Each annotation has its own dedicated method interceptor. -The reason for this is to make things more composable. -For example, if needed, you can disable the Spring Security defaults and <<_enabling_certain_annotations,publish only the `@PostAuthorize` method interceptor>>. - -The method interceptors are as follows: - -* For <>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthenticationManagerBeforeMethodInterceptor#preAuthorize`], which in turn uses {security-api-url}org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.html[`PreAuthorizeAuthorizationManager`] -* For <>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.html[`AuthenticationManagerAfterMethodInterceptor#postAuthorize`], which in turn uses {security-api-url}org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.html[`PostAuthorizeAuthorizationManager`] -* For <>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.html[`PreFilterAuthorizationMethodInterceptor`] -* For <>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.html[`PostFilterAuthorizationMethodInterceptor`] -* For <>, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthenticationManagerBeforeMethodInterceptor#secured`], which in turn uses {security-api-url}org/springframework/security/authorization/method/SecuredAuthorizationManager.html[`SecuredAuthorizationManager`] -* For JSR-250 annotations, Spring Security uses {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.html[`AuthenticationManagerBeforeMethodInterceptor#jsr250`], which in turn uses {security-api-url}org/springframework/security/authorization/method/Jsr250AuthorizationManager.html[`Jsr250AuthorizationManager`] - -Generally speaking, you can consider the following listing as representative of what interceptors Spring Security publishes when you add `@EnableMethodSecurity`: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -@Role(BeanDefinition.ROLE_INFRASTRUCTURE) -static Advisor preAuthorizeMethodInterceptor() { - return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(); -} - -@Bean -@Role(BeanDefinition.ROLE_INFRASTRUCTURE) -static Advisor postAuthorizeMethodInterceptor() { - return AuthorizationManagerAfterMethodInterceptor.postAuthorize(); -} - -@Bean -@Role(BeanDefinition.ROLE_INFRASTRUCTURE) -static Advisor preFilterMethodInterceptor() { - return AuthorizationManagerBeforeMethodInterceptor.preFilter(); -} - -@Bean -@Role(BeanDefinition.ROLE_INFRASTRUCTURE) -static Advisor postFilterMethodInterceptor() { - return AuthorizationManagerAfterMethodInterceptor.postFilter(); -} ----- -====== - -[[favor-granting-authorities]] -=== Favor Granting Authorities Over Complicated SpEL Expressions - -Quite often it can be tempting to introduce a complicated SpEL expression like the following: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')") ----- -====== - -.Kotlin -[source,kotlin,role="kotlin"] ----- -@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')") ----- - -However, you could instead grant `permission:read` to those with `ROLE_ADMIN`. -One way to do this is with a `RoleHierarchy` like so: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -static RoleHierarchy roleHierarchy() { - return new RoleHierarchyImpl("ROLE_ADMIN > permission:read"); -} ----- - -Kotlin:: -+ -[source,java,role="secondary"] ----- -companion object { - @Bean - fun roleHierarchy(): RoleHierarchy { - return RoleHierarchyImpl("ROLE_ADMIN > permission:read") - } -} ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - - ----- -====== - -and then <>. -This then allows you to have a simpler <> expression like this one: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@PreAuthorize("hasAuthority('permission:read')") ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@PreAuthorize("hasAuthority('permission:read')") ----- -====== - -Or, where possible, adapt application-specific authorization logic into granted authorities at login time. - -[[request-vs-method]] -== Comparing Request-level vs Method-level Authorization - -When should you favor method-level authorization over xref:servlet/authorization/authorize-http-requests.adoc[request-level authorization]? -Some of it comes down to taste; however, consider the following strengths list of each to help you decide. - -|=== -|| *request-level* | *method-level* -| *authorization type* | coarse-grained | fine-grained -| *configuration location* | declared in a config class | local to method declaration -| *configuration style* | DSL | Annotations -| *authorization definitions* | programmatic | SpEL -|=== - -The main tradeoff seems to be where you want your authorization rules to live. - -[NOTE] -It's important to remember that when you use annotation-based Method Security, then unannotated methods are not secured. -To protect against this, declare xref:servlet/authorization/authorize-http-requests.adoc#activate-request-security[a catch-all authorization rule] in your xref:servlet/configuration/java.adoc#jc-httpsecurity[`HttpSecurity`] instance. - -[[authorizing-with-annotations]] -== Authorizing with Annotations - -The primary way Spring Security enables method-level authorization support is through annotations that you can add to methods, classes, and interfaces. - -[[use-preauthorize]] -=== Authorizing Method Invocation with `@PreAuthorize` - -When <>, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PreAuthorize.html[`@PreAuthorize`] annotation like so: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Component -public class BankService { - @PreAuthorize("hasRole('ADMIN')") - public Account readAccount(Long id) { - // ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Component -open class BankService { - @PreAuthorize("hasRole('ADMIN')") - fun readAccount(val id: Long): Account { - // ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority - } -} ----- -====== - -This is meant to indicate that the method can only be invoked if the provided expression `hasRole('ADMIN')` passes. - -You can then xref:servlet/test/method.adoc[test the class] to confirm it is enforcing the authorization rule like so: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Autowired -BankService bankService; - -@WithMockUser(roles="ADMIN") -@Test -void readAccountWithAdminRoleThenInvokes() { - Account account = this.bankService.readAccount("12345678"); - // ... assertions -} - -@WithMockUser(roles="WRONG") -@Test -void readAccountWithWrongRoleThenAccessDenied() { - assertThatExceptionOfType(AccessDeniedException.class).isThrownBy( - () -> this.bankService.readAccount("12345678")); -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@WithMockUser(roles="ADMIN") -@Test -fun readAccountWithAdminRoleThenInvokes() { - val account: Account = this.bankService.readAccount("12345678") - // ... assertions -} - -@WithMockUser(roles="WRONG") -@Test -fun readAccountWithWrongRoleThenAccessDenied() { - assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy { - this.bankService.readAccount("12345678") - } -} ----- -====== - -[TIP] -`@PreAuthorize` also can be a <>, be defined <>, and use <>. - -While `@PreAuthorize` is quite helpful for declaring needed authorities, it can also be used to evaluate more complex <>. - -The above two snippets are ensuring that the user can only request orders that belong to them by comparing the username parameter to xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication#getName`]. - -The result is that the above method will only be invoked if the `username` in the request path matches the logged-in user's `name`. -If not, Spring Security will throw an `AccessDeniedException` and return a 403 status code. - -[[use-postauthorize]] -=== Authorization Method Results with `@PostAuthorize` - -When Method Security is active, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PostAuthorize.html[`@PostAuthorize`] annotation like so: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Component -public class BankService { - @PostAuthorize("returnObject.owner == authentication.name") - public Account readAccount(Long id) { - // ... is only returned if the `Account` belongs to the logged in user - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Component -open class BankService { - @PostAuthorize("returnObject.owner == authentication.name") - fun readAccount(val id: Long): Account { - // ... is only returned if the `Account` belongs to the logged in user - } -} ----- -====== - -This is meant to indicate that the method can only return the value if the provided expression `returnObject.owner == authentication.name` passes. -`returnObject` represents the `Account` object to be returned. - -You can then xref:servlet/test/method.adoc[test the class] to confirm it is enforcing the authorization rule: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Autowired -BankService bankService; - -@WithMockUser(username="owner") -@Test -void readAccountWhenOwnedThenReturns() { - Account account = this.bankService.readAccount("12345678"); - // ... assertions -} - -@WithMockUser(username="wrong") -@Test -void readAccountWhenNotOwnedThenAccessDenied() { - assertThatExceptionOfType(AccessDeniedException.class).isThrownBy( - () -> this.bankService.readAccount("12345678")); -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@WithMockUser(username="owner") -@Test -fun readAccountWhenOwnedThenReturns() { - val account: Account = this.bankService.readAccount("12345678") - // ... assertions -} - -@WithMockUser(username="wrong") -@Test -fun readAccountWhenNotOwnedThenAccessDenied() { - assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy { - this.bankService.readAccount("12345678") - } -} ----- -====== - -[TIP] -`@PostAuthorize` also can be a <>, be defined <>, and use <>. - -`@PostAuthorize` is particularly helpful when defending against https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html[Insecure Direct Object Reference]. -In fact, it can be defined as a <> like so: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Target({ ElementType.METHOD, ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) -@PostAuthorize("returnObject.owner == authentication.name") -public @interface RequireOwnership {} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Target(ElementType.METHOD, ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@PostAuthorize("returnObject.owner == authentication.name") -annotation class RequireOwnership ----- -====== - -Allowing you to instead annotate the service in the following way: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Component -public class BankService { - @RequireOwnership - public Account readAccount(Long id) { - // ... is only returned if the `Account` belongs to the logged in user - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Component -open class BankService { - @RequireOwnership - fun readAccount(val id: Long): Account { - // ... is only returned if the `Account` belongs to the logged in user - } -} ----- -====== - -The result is that the above method will only return the `Account` if its `owner` attribute matches the logged-in user's `name`. -If not, Spring Security will throw an `AccessDeniedException` and return a 403 status code. - -[[use-prefilter]] -=== Filtering Method Parameters with `@PreFilter` - -[NOTE] -`@PreFilter` is not yet supported for Kotlin-specific data types; for that reason, only Java snippets are shown - -When Method Security is active, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PreFilter.html[`@PreFilter`] annotation like so: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Component -public class BankService { - @PreFilter("filterObject.owner == authentication.name") - public Collection updateAccounts(Account... accounts) { - // ... `accounts` will only contain the accounts owned by the logged-in user - return updated; - } -} ----- -====== - -This is meant to filter out any values from `accounts` where the expression `filterObject.owner == authentication.name` fails. -`filterObject` represents each `account` in `accounts` and is used to test each `account`. - -You can then test the class in the following way to confirm it is enforcing the authorization rule: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Autowired -BankService bankService; - -@WithMockUser(username="owner") -@Test -void updateAccountsWhenOwnedThenReturns() { - Account ownedBy = ... - Account notOwnedBy = ... - Collection updated = this.bankService.updateAccounts(ownedBy, notOwnedBy); - assertThat(updated).containsOnly(ownedBy); -} ----- -====== - -[TIP] -`@PreFilter` also can be a <>, be defined <>, and use <>. - -`@PreFilter` supports arrays, collections, maps, and streams (so long as the stream is still open). - -For example, the above `updateAccounts` declaration will function the same way as the following other four: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@PreFilter("filterObject.owner == authentication.name") -public Collection updateAccounts(Account[] accounts) - -@PreFilter("filterObject.owner == authentication.name") -public Collection updateAccounts(Collection accounts) - -@PreFilter("filterObject.value.owner == authentication.name") -public Collection updateAccounts(Map accounts) - -@PreFilter("filterObject.owner == authentication.name") -public Collection updateAccounts(Stream accounts) ----- -====== - -The result is that the above method will only have the `Account` instances where their `owner` attribute matches the logged-in user's `name`. - -[[use-postfilter]] -=== Filtering Method Results with `@PostFilter` - -[NOTE] -`@PostFilter` is not yet supported for Kotlin-specific data types; for that reason, only Java snippets are shown - -When Method Security is active, you can annotate a method with the {security-api-url}org/springframework/security/access/prepost/PostFilter.html[`@PostFilter`] annotation like so: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Component -public class BankService { - @PostFilter("filterObject.owner == authentication.name") - public Collection readAccounts(String... ids) { - // ... the return value will be filtered to only contain the accounts owned by the logged-in user - return accounts; - } -} ----- -====== - -This is meant to filter out any values from the return value where the expression `filterObject.owner == authentication.name` fails. -`filterObject` represents each `account` in `accounts` and is used to test each `account`. - -You can then test the class like so to confirm it is enforcing the authorization rule: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Autowired -BankService bankService; - -@WithMockUser(username="owner") -@Test -void readAccountsWhenOwnedThenReturns() { - Collection accounts = this.bankService.updateAccounts("owner", "not-owner"); - assertThat(accounts).hasSize(1); - assertThat(accounts.get(0).getOwner()).isEqualTo("owner"); -} ----- -====== - -[TIP] -`@PostFilter` also can be a <>, be defined <>, and use <>. - -`@PostFilter` supports arrays, collections, maps, and streams (so long as the stream is still open). - -For example, the above `readAccounts` declaration will function the same way as the following other three: - -```java -@PostFilter("filterObject.owner == authentication.name") -public Account[] readAccounts(String... ids) - -@PostFilter("filterObject.value.owner == authentication.name") -public Map readAccounts(String... ids) - -@PostFilter("filterObject.owner == authentication.name") -public Stream readAccounts(String... ids) -``` - -The result is that the above method will return the `Account` instances where their `owner` attribute matches the logged-in user's `name`. - -[NOTE] -In-memory filtering can obviously be expensive, and so be considerate of whether it is better to xref:servlet/integrations/data.adoc[filter the data in the data layer] instead. - -[[use-secured]] -=== Authorizing Method Invocation with `@Secured` - -{security-api-url}org/springframework/security/access/annotation/Secured.html[`@Secured`] is a legacy option for authorizing invocations. -<> supercedes it and is recommended instead. - -To use the `@Secured` annotation, you should first change your Method Security declaration to enable it like so: +You can enable support for Spring Security's `@Secured` annotation using: +.@Secured Configuration [tabs] ====== Java:: + [source,java,role="primary"] ---- +@Configuration @EnableMethodSecurity(securedEnabled = true) +public class MethodSecurityConfig { + // ... +} ---- Kotlin:: + [source,kotlin,role="secondary"] ---- +@Configuration @EnableMethodSecurity(securedEnabled = true) +class MethodSecurityConfig { + // ... +} ---- Xml:: @@ -729,30 +137,31 @@ Xml:: ---- ====== -This will cause Spring Security to publish <> that authorizes methods, classes, and interfaces annotated with `@Secured`. - -[[use-jsr250]] -=== Authorizing Method Invocation with JSR-250 Annotations - -In case you would like to use https://jcp.org/en/jsr/detail?id=250[JSR-250] annotations, Spring Security also supports that. -<> has more expressive power and is thus recommended. - -To use the JSR-250 annotations, you should first change your Method Security declaration to enable them like so: +or JSR-250 using: +.JSR-250 Configuration [tabs] ====== Java:: + [source,java,role="primary"] ---- +@Configuration @EnableMethodSecurity(jsr250Enabled = true) +public class MethodSecurityConfig { + // ... +} ---- Kotlin:: + [source,kotlin,role="secondary"] ---- +@Configuration @EnableMethodSecurity(jsr250Enabled = true) +class MethodSecurityConfig { + // ... +} ---- Xml:: @@ -763,434 +172,12 @@ Xml:: ---- ====== -This will cause Spring Security to publish <> that authorizes methods, classes, and interfaces annotated with `@RolesAllowed`, `@PermitAll`, and `@DenyAll`. +=== Customizing Authorization +Spring Security's `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` ship with rich expression-based support. -[[class-or-interface-annotations]] -=== Declaring Annotations at the Class or Interface Level - -It's also supported to have Method Security annotations at the class and interface level. - -If it is at the class level like so: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Controller -@PreAuthorize("hasAuthority('ROLE_USER')") -public class MyController { - @GetMapping("/endpoint") - public String endpoint() { ... } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Controller -@PreAuthorize("hasAuthority('ROLE_USER')") -open class MyController { - @GetMapping("/endpoint") - fun endpoint(): String { ... } -} ----- -====== - -then all methods inherit the class-level behavior. - -Or, if it's declared like the following at both the class and method level: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Controller -@PreAuthorize("hasAuthority('ROLE_USER')") -public class MyController { - @GetMapping("/endpoint") - @PreAuthorize("hasAuthority('ROLE_ADMIN')") - public String endpoint() { ... } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Controller -@PreAuthorize("hasAuthority('ROLE_USER')") -open class MyController { - @GetMapping("/endpoint") - @PreAuthorize("hasAuthority('ROLE_ADMIN')") - fun endpoint(): String { ... } -} ----- -====== - -then methods declaring the annotation override the class-level annotation. - -The same is true for interfaces, with the exception that if a class inherits the annotation from two different interfaces, then startup will fail. -This is because Spring Security has no way to tell which one you want to use. - -In cases like this, you can resolve the ambiguity by adding the annotation to the concrete method. - -[[meta-annotations]] -=== Using Meta Annotations - -Method Security supports meta annotations. -This means that you can take any annotation and improve readability based on your application-specific use cases. - -For example, you can simplify `@PreAuthorize("hasRole('ADMIN')")` to `@IsAdmin` like so: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Target({ ElementType.METHOD, ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) -@PreAuthorize("hasRole('ADMIN')") -public @interface IsAdmin {} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Target(ElementType.METHOD, ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@PreAuthorize("hasRole('ADMIN')") -annotation class IsAdmin ----- -====== - -And the result is that on your secured methods you can now do the following instead: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Component -public class BankService { - @IsAdmin - public Account readAccount(Long id) { - // ... is only returned if the `Account` belongs to the logged in user - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Component -open class BankService { - @IsAdmin - fun readAccount(val id: Long): Account { - // ... is only returned if the `Account` belongs to the logged in user - } -} ----- -====== - -This results in more readable method definitions. - -[[enable-annotation]] -=== Enabling Certain Annotations - -You can turn off ``@EnableMethodSecurity``'s pre-configuration and replace it with you own. -You may choose to do this if you want to <> or `Pointcut`. -Or you may simply want to only enable a specific annotation, like `@PostAuthorize`. - -You can do this in the following way: - -.Only @PostAuthorize Configuration -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableMethodSecurity(prePostEnabled = false) -class MethodSecurityConfig { - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - Advisor postAuthorize() { - return AuthorizationManagerBeforeMethodInterceptor.postAuthorize(); - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -@EnableMethodSecurity(prePostEnabled = false) -class MethodSecurityConfig { - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - fun postAuthorize() : Advisor { - return AuthorizationManagerBeforeMethodInterceptor.postAuthorize() - } -} ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - - - - ----- -====== - -The above snippet achieves this by first disabling Method Security's pre-configurations and then publishing <> itself. - -[[use-intercept-methods]] -== Authorizing with `` - -While using Spring Security's <> is preferred for method security, you can also use XML to declare bean authorization rules. - -If you need to declare it in your XML configuration instead, you can use xref:servlet/appendix/namespace/method-security.adoc#nsa-intercept-methods[``] like so: - -[tabs] -====== -Xml:: -+ -[source,xml,role="primary"] ----- - - - - - - ----- -====== - -[NOTE] -This only supports matching method by prefix or by name. -If your needs are more complex than that, <> instead. - -[[use-programmatic-authorization]] -== Authorizing Methods Programmatically - -As you've already seen, there are several ways that you can specify non-trivial authorization rules using <>. - -There are a number of ways that you can instead allow your logic to be Java-based instead of SpEL-based. -This gives use access the entire Java language for increased testability and flow control. - -=== Using a Custom Bean in SpEL - -The first way to authorize a method programmatically is a two-step process. - -First, declare a bean that has a method that takes a `MethodSecurityExpressionOperations` instance like the following: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Component("authz") -public class AuthorizationLogic { - public boolean decide(MethodSecurityExpressionOperations operations) { - // ... authorization logic - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Component("authz") -open class AuthorizationLogic { - fun decide(val operations: MethodSecurityExpressionOperations): boolean { - // ... authorization logic - } -} ----- -====== - -Then, reference that bean in your annotations in the following way: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Controller -public class MyController { - @PreAuthorize("@authz.decide(#root)") - @GetMapping("/endpoint") - public String endpoint() { - // ... - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Controller -open class MyController { - @PreAuthorize("@authz.decide(#root)") - @GetMapping("/endpoint") - fun String endpoint() { - // ... - } -} ----- -====== - -Spring Security will invoke the given method on that bean for each method invocation. - -What's nice about this is all your authorization logic is in a separate class that can be independently unit tested and verified for correctness. -It also has access to the full Java language. - -[[custom-authorization-managers]] -=== Using a Custom Authorization Manager - -The second way to authorize a method programmatically is to create a custom xref:servlet/authorization/architecture.adoc#_the_authorizationmanager[`AuthorizationManager`]. - -First, declare an authorization manager instance, perhaps like this one: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Component -public class MyAuthorizationManager implements AuthorizationManager, AuthorizationManager { - @Override - public AuthorizationDecision check(Supplier authentication, MethodInvocation invocation) { - // ... authorization logic - } - - @Override - public AuthorizationDecision check(Supplier authentication, MethodInvocationResult invocation) { - // ... authorization logic - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Component -class MyAuthorizationManager : AuthorizationManager, AuthorizationManager { - override fun check(authentication: Supplier, invocation: MethodInvocation): AuthorizationDecision { - // ... authorization logic - } - - override fun check(authentication: Supplier, invocation: MethodInvocationResult): AuthorizationDecision { - // ... authorization logic - } -} ----- -====== - -Then, publish the method interceptor with a pointcut that corresponds to when you want that `AuthorizationManager` to run. -For example, you could replace how `@PreAuthorize` and `@PostAuthorize` work like so: - -.Only @PreAuthorize and @PostAuthorize Configuration -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableMethodSecurity(prePostEnabled = false) -class MethodSecurityConfig { - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - Advisor preAuthorize(MyAuthorizationManager manager) { - return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager); - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - Advisor postAuthorize(MyAuthorizationManager manager) { - return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager); - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -@EnableMethodSecurity(prePostEnabled = false) -class MethodSecurityConfig { - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - fun preAuthorize(val manager: MyAuthorizationManager) : Advisor { - return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager) - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - fun postAuthorize(val manager: MyAuthorizationManager) : Advisor { - return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager) - } -} ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - - - - - - - - - - ----- -====== - -[TIP] -==== -You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`. -==== - -[[customizing-expression-handling]] -=== Customizing Expression Handling - -Or, third, you can customize how each SpEL expression is handled. -To do that, you can expose a custom {security-api-url}org.springframework.security.access.expression.method.MethodSecurityExpressionHandler.html[`MethodSecurityExpressionHandler`], like so: +[[jc-method-security-custom-expression-handler]] +If you need to customize the way that expressions are handled, you can expose a custom `MethodSecurityExpressionHandler`, like so: .Custom MethodSecurityExpressionHandler [tabs] @@ -1200,9 +187,9 @@ Java:: [source,java,role="primary"] ---- @Bean -static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) { +static MethodSecurityExpressionHandler methodSecurityExpressionHandler() { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); - handler.setRoleHierarchy(roleHierarchy); + handler.setTrustResolver(myCustomTrustResolver); return handler; } ---- @@ -1213,9 +200,9 @@ Kotlin:: ---- companion object { @Bean - fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler { + fun methodSecurityExpressionHandler() : MethodSecurityExpressionHandler { val handler = DefaultMethodSecurityExpressionHandler(); - handler.setRoleHierarchy(roleHierarchy); + handler.setTrustResolver(myCustomTrustResolver); return handler; } } @@ -1231,7 +218,7 @@ Xml:: - + ---- ====== @@ -1241,33 +228,21 @@ Xml:: We expose `MethodSecurityExpressionHandler` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes ==== -You can also <> to add your own custom authorization expressions beyond the defaults. +Also, for role-based authorization, Spring Security adds a default `ROLE_` prefix, which is uses when evaluating expressions like `hasRole`. -[[use-aspectj]] -== Authorizing with AspectJ - -[[match-by-pointcut]] -=== Matching Methods with Custom Pointcuts - -Being built on Spring AOP, you can declare patterns that are not related to annotations, similar to xref:servlet/authorization/authorize-http-requests.adoc[request-level authorization]. -This has the potential advantage of centralizing method-level authorization rules. - -For example, you can use publish your own `Advisor` or use xref:servlet/appendix/namespace/method-security.adoc#nsa-protect-pointcut[``] to match AOP expressions to authorization rules for your service layer like so: +[[jc-method-security-custom-granted-authority-defaults]] +You can configure the authorization rules to use a different prefix by exposing a `GrantedAuthorityDefaults` bean, like so: +.Custom MethodSecurityExpressionHandler [tabs] ====== Java:: + [source,java,role="primary"] ---- -import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole; - @Bean -@Role(BeanDefinition.ROLE_INFRASTRUCTURE) -static Advisor protectServicePointcut() { - JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut(); - pattern.setPattern("execution(* com.mycompany.*Service.*(..))"); - return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER")); +static GrantedAuthorityDefaults grantedAuthorityDefaults() { + return new GrantedAuthorityDefaults("MYPREFIX_"); } ---- @@ -1275,33 +250,634 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole; - companion object { - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - fun protectServicePointcut(): Advisor { - var pattern = JdkRegexpMethodPointcut(); - pattern.setPattern("execution(* com.mycompany.*Service.*(..))"); - return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER")); + @Bean + fun grantedAuthorityDefaults() : GrantedAuthorityDefaults { + return GrantedAuthorityDefaults("MYPREFIX_"); + } +} +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + + + + + +---- +====== + +[TIP] +==== +We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes +==== + +[[jc-method-security-custom-authorization-manager]] +=== Custom Authorization Managers + +Method authorization is a combination of before- and after-method authorization. + +[NOTE] +==== +Before-method authorization is performed before the method is invoked. +If that authorization denies access, the method is not invoked, and an `AccessDeniedException` is thrown. +After-method authorization is performed after the method is invoked, but before the method returns to the caller. +If that authorization denies access, the value is not returned, and an `AccessDeniedException` is thrown +==== + +To recreate what adding `@EnableMethodSecurity` does by default, you would publish the following configuration: + +.Full Pre-post Method Security Configuration +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableMethodSecurity(prePostEnabled = false) +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor preFilterAuthorizationMethodInterceptor() { + return new PreFilterAuthorizationMethodInterceptor(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor preAuthorizeAuthorizationMethodInterceptor() { + return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor postAuthorizeAuthorizationMethodInterceptor() { + return AuthorizationManagerAfterMethodInterceptor.postAuthorize(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor postFilterAuthorizationMethodInterceptor() { + return new PostFilterAuthorizationMethodInterceptor(); + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableMethodSecurity(prePostEnabled = false) +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun preFilterAuthorizationMethodInterceptor() : Advisor { + return PreFilterAuthorizationMethodInterceptor(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun preAuthorizeAuthorizationMethodInterceptor() : Advisor { + return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun postAuthorizeAuthorizationMethodInterceptor() : Advisor { + return AuthorizationManagerAfterMethodInterceptor.postAuthorize(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun postFilterAuthorizationMethodInterceptor() : Advisor { + return PostFilterAuthorizationMethodInterceptor(); + } +} +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + + + + + + + + +---- +====== + +Notice that Spring Security's method security is built using Spring AOP. +So, interceptors are invoked based on the order specified. +This can be customized by calling `setOrder` on the interceptor instances like so: + +.Publish Custom Advisor +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +Advisor postFilterAuthorizationMethodInterceptor() { + PostFilterAuthorizationMethodInterceptor interceptor = new PostFilterAuthorizationMethodInterceptor(); + interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1); + return interceptor; +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +fun postFilterAuthorizationMethodInterceptor() : Advisor { + val interceptor = PostFilterAuthorizationMethodInterceptor(); + interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1); + return interceptor; +} +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + + + +---- +====== + +You may want to only support `@PreAuthorize` in your application, in which case you can do the following: + + +.Only @PreAuthorize Configuration +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableMethodSecurity(prePostEnabled = false) +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor preAuthorize() { + return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(); + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableMethodSecurity(prePostEnabled = false) +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun preAuthorize() : Advisor { + return AuthorizationManagerBeforeMethodInterceptor.preAuthorize() + } +} +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + + + + + +---- +====== + +Or, you may have a custom before-method `AuthorizationManager` that you want to add to the list. + +In this case, you will need to tell Spring Security both the `AuthorizationManager` and to which methods and classes your authorization manager applies. + +Thus, you can configure Spring Security to invoke your `AuthorizationManager` in between `@PreAuthorize` and `@PostAuthorize` like so: + +.Custom Before Advisor + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableMethodSecurity +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public Advisor customAuthorize() { + JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut(); + pattern.setPattern("org.mycompany.myapp.service.*"); + AuthorizationManager rule = AuthorityAuthorizationManager.isAuthenticated(); + AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(pattern, rule); + interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1); + return interceptor; + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableMethodSecurity +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun customAuthorize() : Advisor { + val pattern = JdkRegexpMethodPointcut(); + pattern.setPattern("org.mycompany.myapp.service.*"); + val rule = AuthorityAuthorizationManager.isAuthenticated(); + val interceptor = AuthorizationManagerBeforeMethodInterceptor(pattern, rule); + interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1); + return interceptor; + } +} +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + + + + + + + + + + + + + + + +---- +====== + +[TIP] +==== +You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`. +==== + +The same can be done for after-method authorization. +After-method authorization is generally concerned with analysing the return value to verify access. + +For example, you might have a method that confirms that the account requested actually belongs to the logged-in user like so: + +.@PostAuthorize example +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +public interface BankService { + + @PreAuthorize("hasRole('USER')") + @PostAuthorize("returnObject.owner == authentication.name") + Account readAccount(Long id); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +interface BankService { + + @PreAuthorize("hasRole('USER')") + @PostAuthorize("returnObject.owner == authentication.name") + fun readAccount(id : Long) : Account +} +---- +====== + +You can supply your own `AuthorizationMethodInterceptor` to customize how access to the return value is evaluated. + +For example, if you have your own custom annotation, you can configure it like so: + + +.Custom After Advisor +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableMethodSecurity +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public Advisor customAuthorize(AuthorizationManager rules) { + AnnotationMatchingPointcut pattern = new AnnotationMatchingPointcut(MySecurityAnnotation.class); + AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(pattern, rules); + interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1); + return interceptor; + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableMethodSecurity +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun customAuthorize(rules : AuthorizationManager) : Advisor { + val pattern = AnnotationMatchingPointcut(MySecurityAnnotation::class.java); + val interceptor = AuthorizationManagerAfterMethodInterceptor(pattern, rules); + interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1); + return interceptor; + } +} +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + + + + + + + + + + + + + + + +---- +====== + +and it will be invoked after the `@PostAuthorize` interceptor. + +[[jc-enable-global-method-security]] +== EnableGlobalMethodSecurity + +We can enable annotation-based security by using the `@EnableGlobalMethodSecurity` annotation on any `@Configuration` instance. +The following example enables Spring Security's `@Secured` annotation: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableGlobalMethodSecurity(securedEnabled = true) +public class MethodSecurityConfig { +// ... +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableGlobalMethodSecurity(securedEnabled = true) +open class MethodSecurityConfig { + // ... +} +---- +====== + +Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly. +Spring Security's native annotation support defines a set of attributes for the method. +These are passed to the `AccessDecisionManager` for it to make the actual decision: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +public interface BankService { + +@Secured("IS_AUTHENTICATED_ANONYMOUSLY") +public Account readAccount(Long id); + +@Secured("IS_AUTHENTICATED_ANONYMOUSLY") +public Account[] findAccounts(); + +@Secured("ROLE_TELLER") +public Account post(Account account, double amount); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +interface BankService { + @Secured("IS_AUTHENTICATED_ANONYMOUSLY") + fun readAccount(id: Long): Account + + @Secured("IS_AUTHENTICATED_ANONYMOUSLY") + fun findAccounts(): Array + + @Secured("ROLE_TELLER") + fun post(account: Account, amount: Double): Account +} +---- +====== + +Support for JSR-250 annotations can be enabled by using: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableGlobalMethodSecurity(jsr250Enabled = true) +public class MethodSecurityConfig { +// ... +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableGlobalMethodSecurity(jsr250Enabled = true) +open class MethodSecurityConfig { + // ... +} +---- +====== + +These are standards-based and let simple role-based constraints be applied but do not have the power Spring Security's native annotations. +To use the new expression-based syntax, you would use: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class MethodSecurityConfig { +// ... +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true) +open class MethodSecurityConfig { + // ... +} +---- +====== + +The equivalent Java code is: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +public interface BankService { + +@PreAuthorize("isAnonymous()") +public Account readAccount(Long id); + +@PreAuthorize("isAnonymous()") +public Account[] findAccounts(); + +@PreAuthorize("hasAuthority('ROLE_TELLER')") +public Account post(Account account, double amount); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +interface BankService { + @PreAuthorize("isAnonymous()") + fun readAccount(id: Long): Account + + @PreAuthorize("isAnonymous()") + fun findAccounts(): Array + + @PreAuthorize("hasAuthority('ROLE_TELLER')") + fun post(account: Account, amount: Double): Account +} +---- +====== + +== GlobalMethodSecurityConfiguration + +Sometimes, you may need to perform operations that are more complicated than are possible with the `@EnableGlobalMethodSecurity` annotation. +For these instances, you can extend the `GlobalMethodSecurityConfiguration`, ensuring that the `@EnableGlobalMethodSecurity` annotation is present on your subclass. +For example, if you wanted to provide a custom `MethodSecurityExpressionHandler`, you could use the following configuration: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { + @Override + protected MethodSecurityExpressionHandler createExpressionHandler() { + // ... create and return custom MethodSecurityExpressionHandler ... + return expressionHandler; + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true) +open class MethodSecurityConfig : GlobalMethodSecurityConfiguration() { + override fun createExpressionHandler(): MethodSecurityExpressionHandler { + // ... create and return custom MethodSecurityExpressionHandler ... + return expressionHandler } } ---- ====== +For additional information about methods that can be overridden, see the Javadoc for the {security-api-url}org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.html[`GlobalMethodSecurityConfiguration`] class. + +[[ns-global-method]] +== The Element +This element is used to enable annotation-based security in your application (by setting the appropriate attributes on the element) and to group together security pointcut declarations that are applied across your entire application context. +You should only declare one `` element. +The following declaration enables support for Spring Security's `@Secured`: + [source,xml] ---- - - - + ---- -[[weave-aspectj]] -=== Integrate with AspectJ Byte-weaving - -Performance can at times be enhanced by using AspectJ to weave Spring Security advice into the byte code of your beans. - -After setting up AspectJ, you can quite simply state in the `@EnableMethodSecurity` annotation or `` element that you are using AspectJ: +Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly. +Spring Security's native annotation support defines a set of attributes for the method. +These are passed to the `AccessDecisionManager` for it to make the actual decision. +The following example shows the `@Secured` annotation in a typical interface: [tabs] ====== @@ -1309,40 +885,53 @@ Java:: + [source,java,role="primary"] ---- -@EnableMethodSecurity(mode=AdviceMode.ASPECTJ) +public interface BankService { + +@Secured("IS_AUTHENTICATED_ANONYMOUSLY") +public Account readAccount(Long id); + +@Secured("IS_AUTHENTICATED_ANONYMOUSLY") +public Account[] findAccounts(); + +@Secured("ROLE_TELLER") +public Account post(Account account, double amount); +} ---- + Kotlin:: + [source,kotlin,role="secondary"] ---- -@EnableMethodSecurity(mode=AdviceMode.ASPECTJ) ----- +interface BankService { + @Secured("IS_AUTHENTICATED_ANONYMOUSLY") + fun readAccount(id: Long): Account -Xml:: -+ -[source,xml,role="secondary"] ----- - + @Secured("IS_AUTHENTICATED_ANONYMOUSLY") + fun findAccounts(): Array + + @Secured("ROLE_TELLER") + fun post(account: Account, amount: Double): Account +} ---- ====== -And the result will be that Spring Security will publish its advisors as AspectJ advice so that they can be woven in accordingly. +Support for JSR-250 annotations can be enabled by using: -[[changing-the-order]] -== Specifying Order +[source,xml] +---- + +---- -As already noted, there is a Spring AOP method interceptor for each annotation, and each of these has a location in the Spring AOP advisor chain. +These are standards-based and allow simple role-based constraints to be applied, but they do not have the power Spring Security's native annotations. +To use the expression-based syntax, use: -Namely, the `@PreFilter` method interceptor's order is 100, ``@PreAuthorize``'s is 200, and so on. +[source,xml] +---- + +---- -The reason this is important to note is that there are other AOP-based annotations like `@EnableTransactionManagement` that have an order of `Integer.MAX_VALUE`. -In other words, they are located at the end of the advisor chain by default. - -At times, it can be valuable to have other advice execute before Spring Security. -For example, if you have a method annotated with `@Transactional` and `@PostAuthorize`, you might want the transaction to still be open when `@PostAuthorize` runs so that an `AccessDeniedException` will cause a rollback. - -To get `@EnableTransactionManagement` to open a transaction before method authorization advice runs, you can set ``@EnableTransactionManagement``'s order like so: +The equivalent Java code is: [tabs] ====== @@ -1350,79 +939,16 @@ Java:: + [source,java,role="primary"] ---- -@EnableTransactionManagement(order = 0) ----- +public interface BankService { -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@EnableTransactionManagement(order = 0) ----- +@PreAuthorize("isAnonymous()") +public Account readAccount(Long id); -Xml:: -+ -[source,xml,role="secondary"] ----- - ----- -====== +@PreAuthorize("isAnonymous()") +public Account[] findAccounts(); -Since the earliest method interceptor (`@PreFilter`) is set to an order of 100, a setting of zero means that the transaction advice will run before all Spring Security advice. - -[[authorization-expressions]] -== Expressing Authorization with SpEL - -You've already seen several examples using SpEL, so now let's cover the API a bit more in depth. - -Spring Security encapsulates all of its authorization fields and methods in a set of root objects. -The most generic root object is called `SecurityExpressionRoot` and it forms the basis for `MethodSecurityExpressionRoot`. -Spring Security supplies this root object to `MethodSecurityEvaluationContext` when preparing to evaluate an authorization expression. - -[[using-authorization-expression-fields-and-methods]] -=== Using Authorization Expression Fields and Methods - -The first thing this provides is an enhanced set of authorization fields and methods to your SpEL expressions. -What follows is a quick overview of the most common methods: - -* `permitAll` - The method requires no authorization to be invoked; note that in this case, xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] is never retrieved from the session -* `denyAll` - The method is not allowed under any circumstances; note that in this case, the `Authentication` is never retrieved from the session -* `hasAuthority` - The method requires that the `Authentication` have xref:servlet/authorization/architecture.adoc#authz-authorities[a `GrantedAuthority`] that matches the given value -* `hasRole` - A shortcut for `hasAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix -* `hasAnyAuthority` - The method requires that the `Authentication` have a `GrantedAuthority` that matches any of the given values -* `hasAnyRole` - A shortcut for `hasAnyAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix -* `hasPermission` - A hook into your `PermissionEvaluator` instance for doing object-level authorization - -And here is a brief look at the most common fields: - -* `authentication` - The `Authentication` instance associated with this method invocation -* `principal` - The `Authentication#getPrincipal` associated with this method invocation - -Having now learned the patterns, rules, and how they can be paired together, you should be able to understand what is going on in this more complex example: - -.Authorize Requests -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Component -public class MyService { - @PreAuthorize("denyAll") <1> - MyResource myDeprecatedMethod(...); - - @PreAuthorize("hasRole('ADMIN')") <2> - MyResource writeResource(...) - - @PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") <3> - MyResource deleteResource(...) - - @PreAuthorize("principal.claims['aud'] == 'my-audience'") <4> - MyResource readResource(...); - - @PreAuthorize("@authz.check(authentication, #root)") - MyResource shareResource(...); +@PreAuthorize("hasAuthority('ROLE_TELLER')") +public Account post(Account account, double amount); } ---- @@ -1430,353 +956,49 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -@Component -open class MyService { - @PreAuthorize("denyAll") <1> - fun myDeprecatedMethod(...): MyResource +interface BankService { + @PreAuthorize("isAnonymous()") + fun readAccount(id: Long): Account - @PreAuthorize("hasRole('ADMIN')") <2> - fun writeResource(...): MyResource + @PreAuthorize("isAnonymous()") + fun findAccounts(): Array - @PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") <3> - fun deleteResource(...): MyResource - - @PreAuthorize("principal.claims['aud'] == 'my-audience'") <4> - fun readResource(...): MyResource - - @PreAuthorize("@authz.check(#root)") - fun shareResource(...): MyResource; -} ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - - <1> - <2> - <3> - <4> - <5> - ----- -====== -<1> This method may not be invoked by anyone for any reason -<2> This method may only be invoked by ``Authentication``s granted the `ROLE_ADMIN` authority -<3> This method may only be invoked by ``Authentication``s granted the `db` and `ROLE_ADMIN` authorities -<4> This method may only be invoked by ``Princpal``s with an `aud` claim equal to "my-audience" -<5> This method may only be invoked if the bean ``authz``'s `check` method returns `true` - -[[using_method_parameters]] -=== Using Method Parameters - -Additionally, Spring Security provides a mechanism for discovering method parameters so they can also be accessed in the SpEL expression as well. - -For a complete reference, Spring Security uses `DefaultSecurityParameterNameDiscoverer` to discover the parameter names. -By default, the following options are tried for a method. - -1. If Spring Security's `@P` annotation is present on a single argument to the method, the value is used. -The following example uses the `@P` annotation: - -+ - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -import org.springframework.security.access.method.P; - -... - -@PreAuthorize("hasPermission(#c, 'write')") -public void updateContact(@P("c") Contact contact); ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -import org.springframework.security.access.method.P - -... - -@PreAuthorize("hasPermission(#c, 'write')") -fun doSomething(@P("c") contact: Contact?) ----- -====== -+ -The intention of this expression is to require that the current `Authentication` have `write` permission specifically for this `Contact` instance. -+ -Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation. - -* If xref:servlet/integrations/data.adoc[Spring Data's] `@Param` annotation is present on at least one parameter for the method, the value is used. -The following example uses the `@Param` annotation: -+ -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -import org.springframework.data.repository.query.Param; - -... - -@PreAuthorize("#n == authentication.name") -Contact findContactByName(@Param("n") String name); ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -import org.springframework.data.repository.query.Param - -... - -@PreAuthorize("#n == authentication.name") -fun findContactByName(@Param("n") name: String?): Contact? ----- -====== -+ -The intention of this expression is to require that `name` be equal to `Authentication#getName` for the invocation to be authorized. -+ -Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation. - -* If you compile your code with the `-parameters` argument, the standard JDK reflection API is used to discover the parameter names. -This works on both classes and interfaces. - -* Finally, if you compile your code with debug symbols, the parameter names are discovered by using the debug symbols. -This does not work for interfaces, since they do not have debug information about the parameter names. -For interfaces, either annotations or the `-parameters` approach must be used. - -[[migration-enableglobalmethodsecurity]] -== Migrating from `@EnableGlobalMethodSecurity` - -If you are using `@EnableGlobalMethodSecurity`, you should migrate to `@EnableMethodSecurity`. - -[[servlet-replace-globalmethodsecurity-with-methodsecurity]] -=== Replace xref:servlet/authorization/method-security.adoc#jc-enable-global-method-security[global method security] with xref:servlet/authorization/method-security.adoc#jc-enable-method-security[method security] - -{security-api-url}org/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.html[`@EnableGlobalMethodSecurity`] and xref:servlet/appendix/namespace/method-security.adoc#nsa-global-method-security[``] are deprecated in favor of {security-api-url}org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.html[`@EnableMethodSecurity`] and xref:servlet/appendix/namespace/method-security.adoc#nsa-method-security[``], respectively. -The new annotation and XML element activate Spring's xref:servlet/authorization/method-security.adoc#jc-enable-method-security[pre-post annotations] by default and use `AuthorizationManager` internally. - -This means that the following two listings are functionally equivalent: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@EnableGlobalMethodSecurity(prePostEnabled = true) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@EnableGlobalMethodSecurity(prePostEnabled = true) ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - ----- -====== - -and: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@EnableMethodSecurity ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@EnableMethodSecurity ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - ----- -====== - -For applications not using the pre-post annotations, make sure to turn it off to avoid activating unwanted behavior. - -For example, a listing like: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@EnableGlobalMethodSecurity(securedEnabled = true) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@EnableGlobalMethodSecurity(securedEnabled = true) ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - ----- -====== - -should change to: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false) ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false) ----- - -Xml:: -+ -[source,xml,role="secondary"] ----- - ----- -====== - -=== Use a Custom `@Bean` instead of subclassing `DefaultMethodSecurityExpressionHandler` - -As a performance optimization, a new method was introduced to `MethodSecurityExpressionHandler` that takes a `Supplier` instead of an `Authentication`. - -This allows Spring Security to defer the lookup of the `Authentication`, and is taken advantage of automatically when you use `@EnableMethodSecurity` instead of `@EnableGlobalMethodSecurity`. - -However, let's say that your code extends `DefaultMethodSecurityExpressionHandler` and overrides `createSecurityExpressionRoot(Authentication, MethodInvocation)` to return a custom `SecurityExpressionRoot` instance. -This will no longer work because the arrangement that `@EnableMethodSecurity` sets up calls `createEvaluationContext(Supplier, MethodInvocation)` instead. - -Happily, such a level of customization is often unnecessary. -Instead, you can create a custom bean with the authorization methods that you need. - -For example, let's say you are wanting a custom evaluation of `@PostAuthorize("hasAuthority('ADMIN')")`. -You can create a custom `@Bean` like this one: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -class MyAuthorizer { - boolean isAdmin(MethodSecurityExpressionOperations root) { - boolean decision = root.hasAuthority("ADMIN"); - // custom work ... - return decision; - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -class MyAuthorizer { - fun isAdmin(val root: MethodSecurityExpressionOperations): boolean { - val decision = root.hasAuthority("ADMIN"); - // custom work ... - return decision; - } + @PreAuthorize("hasAuthority('ROLE_TELLER')") + fun post(account: Account, amount: Double): Account } ---- ====== -and then refer to it in the annotation like so: +Expression-based annotations are a good choice if you need to define simple rules that go beyond checking the role names against the user's list of authorities. -[tabs] -====== -Java:: -+ -[source,java,role="primary"] +[NOTE] +==== +The annotated methods will only be secured for instances which are defined as Spring beans (in the same application context in which method-security is enabled). +If you want to secure instances which are not created by Spring (using the `new` operator, for example) then you need to use AspectJ. +==== + +[NOTE] +==== +You can enable more than one type of annotation in the same application, but only one type should be used for any interface or class as the behaviour will not be well-defined otherwise. +If two annotations are found which apply to a particular method, then only one of them will be applied. +==== + +[[ns-protect-pointcut]] +== Adding Security Pointcuts by using protect-pointcut + +`protect-pointcut` is particularly powerful, as it lets you apply security to many beans with only a simple declaration. +Consider the following example: + +[source,xml] ---- -@PreAuthorize("@authz.isAdmin(#root)") + + + ---- -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@PreAuthorize("@authz.isAdmin(#root)") ----- -====== - -[[subclass-defaultmethodsecurityexpressionhandler]] -==== I'd still prefer to subclass `DefaultMethodSecurityExpressionHandler` - -If you must continue subclassing `DefaultMethodSecurityExpressionHandler`, you can still do so. -Instead, override the `createEvaluationContext(Supplier, MethodInvocation)` method like so: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Component -class MyExpressionHandler extends DefaultMethodSecurityExpressionHandler { - @Override - public EvaluationContext createEvaluationContext(Supplier authentication, MethodInvocation mi) { - StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi); - MethodSecurityExpressionOperations delegate = (MethodSecurityExpressionOperations) context.getRootObject().getValue(); - MySecurityExpressionRoot root = new MySecurityExpressionRoot(delegate); - context.setRootObject(root); - return context; - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Component -class MyExpressionHandler: DefaultMethodSecurityExpressionHandler { - override fun createEvaluationContext(val authentication: Supplier, - val mi: MethodInvocation): EvaluationContext { - val context = super.createEvaluationContext(authentication, mi) as StandardEvaluationContext - val delegate = context.getRootObject().getValue() as MethodSecurityExpressionOperations - val root = MySecurityExpressionRoot(delegate) - context.setRootObject(root); - return context; - } -} ----- -====== - -== Further Reading - -Now that you have secured your application's requests, please xref:servlet/authorization/authorize-http-requests.adoc[secure its requests] if you haven't already. -You can also read further on xref:servlet/test/index.adoc[testing your application] or on integrating Spring Security with other aspects of you application like xref:servlet/integrations/data.adoc[the data layer] or xref:servlet/integrations/observability.adoc[tracing and metrics]. +d. +This configuration protects all methods on beans declared in the application context whose classes are in the `com.mycompany` package and whose class names end in `Service`. +Only users with the `ROLE_USER` role can invoke these methods. +As with URL matching, the most specific matches must come first in the list of pointcuts, as the first matching expression is used. +Security annotations take precedence over pointcuts. diff --git a/docs/modules/ROOT/pages/servlet/authorization/secure-objects.adoc b/docs/modules/ROOT/pages/servlet/authorization/secure-objects.adoc new file mode 100644 index 0000000000..9fc439bba8 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/authorization/secure-objects.adoc @@ -0,0 +1,135 @@ + +[[secure-object-impls]] += Secure Object Implementations + +This section covers how Spring Security handles Secure Object implementations. + +[[aop-alliance]] +== AOP Alliance (MethodInvocation) Security Interceptor +Prior to Spring Security 2.0, securing `MethodInvocation` instances needed a lot of boiler plate configuration. +Now the recommended approach for method security is to use xref:servlet/configuration/xml-namespace.adoc#ns-method-security[namespace configuration]. +This way, the method security infrastructure beans are configured automatically for you, so you need not know about the implementation classes. +We provide only a quick overview of the classes that are involved here. + +Method security is enforced by using a `MethodSecurityInterceptor`, which secures `MethodInvocation` instances. +Depending on the configuration approach, an interceptor may be specific to a single bean or shared between multiple beans. +The interceptor uses a `MethodSecurityMetadataSource` instance to obtain the configuration attributes that apply to a particular method invocation. +`MapBasedMethodSecurityMetadataSource` is used to store configuration attributes keyed by method names (which can be wildcarded) and will be used internally when the attributes are defined in the application context using the `` or `` elements. +Other implementations are used to handle annotation-based configuration. + +=== Explicit MethodSecurityInterceptor Configuration +You can configure a `MethodSecurityInterceptor` directly in your application context for use with one of Spring AOP's proxying mechanisms: + +[source,xml] +---- + + + + + + + + + + + +---- + +[[aspectj]] +== AspectJ (JoinPoint) Security Interceptor +The AspectJ security interceptor is very similar to the AOP Alliance security interceptor discussed in the previous section. +We discuss only the differences in this section. + +The AspectJ interceptor is named `AspectJSecurityInterceptor`. +Unlike the AOP Alliance security interceptor, which relies on the Spring application context to weave in the security interceptor through proxying, the `AspectJSecurityInterceptor` is woven in through the AspectJ compiler. +It would not be uncommon to use both types of security interceptors in the same application, with `AspectJSecurityInterceptor` being used for domain object instance security and the AOP Alliance `MethodSecurityInterceptor` being used for services layer security. + +We first consider how the `AspectJSecurityInterceptor` is configured in the Spring application context: + +[source,xml] +---- + + + + + + + + + + + +---- + +The two interceptors can share the same `securityMetadataSource`, as the `SecurityMetadataSource` works with `java.lang.reflect.Method` instances rather than an AOP library-specific class. +Your access decisions have access to the relevant AOP library-specific invocation (`MethodInvocation` or `JoinPoint`) and can consider a range of additional criteria (such as method arguments) when making access decisions. + +Next, you need to define an AspectJ `aspect`, as the following example shows: + +[source,java] +---- + +package org.springframework.security.samples.aspectj; + +import org.springframework.security.access.intercept.aspectj.AspectJSecurityInterceptor; +import org.springframework.security.access.intercept.aspectj.AspectJCallback; +import org.springframework.beans.factory.InitializingBean; + +public aspect DomainObjectInstanceSecurityAspect implements InitializingBean { + + private AspectJSecurityInterceptor securityInterceptor; + + pointcut domainObjectInstanceExecution(): target(PersistableEntity) + && execution(public * *(..)) && !within(DomainObjectInstanceSecurityAspect); + + Object around(): domainObjectInstanceExecution() { + if (this.securityInterceptor == null) { + return proceed(); + } + + AspectJCallback callback = new AspectJCallback() { + public Object proceedWithObject() { + return proceed(); + } + }; + + return this.securityInterceptor.invoke(thisJoinPoint, callback); + } + + public AspectJSecurityInterceptor getSecurityInterceptor() { + return securityInterceptor; + } + + public void setSecurityInterceptor(AspectJSecurityInterceptor securityInterceptor) { + this.securityInterceptor = securityInterceptor; + } + + public void afterPropertiesSet() throws Exception { + if (this.securityInterceptor == null) + throw new IllegalArgumentException("securityInterceptor required"); + } + } +} +---- + + +In the preceding example, the security interceptor is applied to every instance of `PersistableEntity`, which is an abstract class not shown (you can use any other class or `pointcut` expression you like). +For those curious, `AspectJCallback` is needed because the `proceed();` statement has special meaning only within an `around()` body. +The `AspectJSecurityInterceptor` calls this anonymous `AspectJCallback` class when it wants the target object to continue. + +You need to configure Spring to load the aspect and wire it with the `AspectJSecurityInterceptor`. +The following example shows a bean declaration that achieves this: + +[source,xml] +---- + + + + +---- + +Now you can create your beans from anywhere within your application, using whatever means you think fit (e.g. `new Person();`), and they have the security interceptor applied. diff --git a/docs/modules/ROOT/pages/servlet/configuration/java.adoc b/docs/modules/ROOT/pages/servlet/configuration/java.adoc index 03ac51d827..326eaa333b 100644 --- a/docs/modules/ROOT/pages/servlet/configuration/java.adoc +++ b/docs/modules/ROOT/pages/servlet/configuration/java.adoc @@ -227,11 +227,7 @@ This configuration is considered after `apiFilterChain`, since it has an `@Order You can provide your own custom DSLs in Spring Security: -[tabs] -====== -Java:: -+ -[source,java,role="primary"] +[source,java] ---- public class MyCustomDsl extends AbstractHttpConfigurer { private boolean flag; @@ -264,38 +260,6 @@ public class MyCustomDsl extends AbstractHttpConfigurer() { - var flag: Boolean = false - - override fun init(http: HttpSecurity) { - // any method that adds another configurer - // must be done in the init method - http.csrf().disable() - } - - override fun configure(http: HttpSecurity) { - val context: ApplicationContext = http.getSharedObject(ApplicationContext::class.java) - - // here we lookup from the ApplicationContext. You can also just create a new instance. - val myFilter: MyFilter = context.getBean(MyFilter::class.java) - myFilter.setFlag(flag) - http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter::class.java) - } - - companion object { - @JvmStatic - fun customDsl(): MyCustomDsl { - return MyCustomDsl() - } - } -} ----- -====== - [NOTE] ==== This is actually how methods like `HttpSecurity.authorizeRequests()` are implemented. @@ -303,11 +267,7 @@ This is actually how methods like `HttpSecurity.authorizeRequests()` are impleme You can then use the custom DSL: -[tabs] -====== -Java:: -+ -[source,java,role="primary"] +[source,java] ---- @Configuration @EnableWebSecurity @@ -315,37 +275,15 @@ public class Config { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .with(MyCustomDsl.customDsl(), (dsl) -> dsl + .apply(customDsl()) .flag(true) - ) - // ... + .and() + ...; return http.build(); } } ---- -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -@EnableWebSecurity -class Config { - - @Bean - fun filterChain(http: HttpSecurity): SecurityFilterChain { - http - .with(MyCustomDsl.customDsl()) { - flag = true - } - // ... - - return http.build() - } -} ----- -====== - The code is invoked in the following order: * Code in the `Config.filterChain` method is invoked @@ -363,50 +301,21 @@ org.springframework.security.config.annotation.web.configurers.AbstractHttpConfi You can also explicit disable the default: -[tabs] -====== -Java:: -+ -[source,java,role="primary"] +[source,java] ---- - @Configuration @EnableWebSecurity public class Config { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .with(MyCustomDsl.customDsl(), (dsl) -> dsl - .disable() - ) + .apply(customDsl()).disable() ...; return http.build(); } } ---- -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -@EnableWebSecurity -class Config { - - @Bean - fun filterChain(http: HttpSecurity): SecurityFilterChain { - http - .with(MyCustomDsl.customDsl()) { - disable() - } - // ... - return http.build() - } - -} ----- -====== - [[post-processing-configured-objects]] == Post Processing Configured Objects diff --git a/docs/modules/ROOT/pages/servlet/configuration/xml-namespace.adoc b/docs/modules/ROOT/pages/servlet/configuration/xml-namespace.adoc index e6f2ed028d..1b654377e0 100644 --- a/docs/modules/ROOT/pages/servlet/configuration/xml-namespace.adoc +++ b/docs/modules/ROOT/pages/servlet/configuration/xml-namespace.adoc @@ -137,7 +137,7 @@ The `ROLE_` prefix is a marker that indicates that a simple comparison with the In other words, a normal role-based check should be used. Access-control in Spring Security is not limited to the use of simple roles (hence the use of the prefix to differentiate between different types of security attributes). We see later how the interpretation can vary. The interpretation of the comma-separated values in the `access` attribute depends on the which implementation of the <> is used. -Since Spring Security 3.0, you can also populate the attribute with an xref:servlet/authorization/authorize-http-requests.adoc#authorization-expressions[EL expression]. +Since Spring Security 3.0, you can also populate the attribute with an xref:servlet/authorization/expression-based.adoc#el-access[EL expression]. [NOTE] @@ -369,7 +369,7 @@ If you replace a namespace filter that requires an authentication entry point (t == Method Security Since version 2.0, Spring Security has substantial support for adding security to your service layer methods. It provides support for JSR-250 annotation security as well as the framework's original `@Secured` annotation. -Since version 3.0, you can also make use of xref:servlet/authorization/method-security.adoc#authorizing-with-annotations[expression-based annotations]. +Since version 3.0, you can also make use of xref:servlet/authorization/expression-based.adoc#el-access[expression-based annotations]. You can apply security to a single bean (by using the `intercept-methods` element to decorate the bean declaration), or you can secure multiple beans across the entire service layer using the AspectJ style pointcuts. [[ns-access-manager]] diff --git a/docs/modules/ROOT/pages/servlet/exploits/firewall.adoc b/docs/modules/ROOT/pages/servlet/exploits/firewall.adoc index 4ab70e3669..a334a409d8 100644 --- a/docs/modules/ROOT/pages/servlet/exploits/firewall.adoc +++ b/docs/modules/ROOT/pages/servlet/exploits/firewall.adoc @@ -111,7 +111,7 @@ XML:: ---- + p:allowedHttpMethods="GET,HEAD"/> ---- diff --git a/docs/modules/ROOT/pages/servlet/getting-started.adoc b/docs/modules/ROOT/pages/servlet/getting-started.adoc index 59b17b6781..a3347e21c4 100644 --- a/docs/modules/ROOT/pages/servlet/getting-started.adoc +++ b/docs/modules/ROOT/pages/servlet/getting-started.adoc @@ -1,174 +1,71 @@ [[servlet-hello]] = Hello Spring Security -This section covers the minimum setup for how to use Spring Security with {spring-boot-reference-url}[Spring Boot] and then points you to next steps after that. +This section covers the minimum setup for how to use Spring Security with Spring Boot. [NOTE] ==== -The completed starter application can be found {gh-samples-url}/servlet/spring-boot/java/hello-security[in our samples repository]. -For your convenience, you can download a minimal Spring Boot + Spring Security application https://start.spring.io/starter.zip?type=maven-project&language=java&packaging=jar&jvmVersion=1.8&groupId=example&artifactId=hello-security&name=hello-security&description=Hello%20Security&packageName=example.hello-security&dependencies=web,security[prepared by Spring Initializr]. +The completed application can be found {gh-samples-url}/servlet/spring-boot/java/hello-security[in our samples repository]. +For your convenience, you can download https://start.spring.io/starter.zip?type=maven-project&language=java&packaging=jar&jvmVersion=1.8&groupId=example&artifactId=hello-security&name=hello-security&description=Hello%20Security&packageName=example.hello-security&dependencies=web,security[a minimal Spring Boot + Spring Security application]. ==== [[servlet-hello-dependencies]] == Updating Dependencies -You first need to add Spring Security to your application's classpath; two ways to do this are to xref:getting-spring-security.adoc#getting-maven-boot[use Maven] or xref:getting-spring-security.adoc#getting-gradle-boot[Gradle]. +The only step you need to do is update the dependencies by using xref:getting-spring-security.adoc#getting-maven-boot[Maven] or xref:getting-spring-security.adoc#getting-gradle-boot[Gradle]. [[servlet-hello-starting]] == Starting Hello Spring Security Boot -With Spring Security <>, you can now {spring-boot-reference-url}#using.running-your-application[run the Spring Boot application]. -The following snippet shows some of the output that indicates that Spring Security is enabled in your application: +You can now https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-running-with-the-maven-plugin[run the Spring Boot application] by using the Maven Plugin's `run` goal. +The following example shows how to do so (and the beginning of the output from doing so): .Running Spring Boot Application -[tabs] -====== -Maven:: -+ -[source,bash,role="primary"] ----- -$ ./mvnw spring-boot:run -... -INFO 23689 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration : - -Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336 - -... ----- - -Gradle:: -+ -[source,bash,role="secondary"] ----- -$ ./gradlew :bootRun -... -INFO 23689 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration : - -Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336 - -... ----- - -Jar:: -+ -[source,bash,role="secondary"] ----- -$ java -jar target/myapplication-0.0.1.jar -... -INFO 23689 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration : - -Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336 - -... ----- -====== - -Now that you have it running, you might try hitting an endpoint to see what happens. -If you hit an endpoint without credentials like so: - -.Querying a Secured Boot Application [source,bash] ---- -$ curl -i http://localhost:8080/some/path -HTTP/1.1 401 +$ ./mvn spring-boot:run +... +INFO 23689 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration : + +Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336 + ... ---- -then Spring Security denies access with a `401 Unauthorized`. -[TIP] -If you provide the same URL in a browser, it will redirect to a default login page. - -And if you hit an endpoint with credentials (found in the console output) as follows: - -.Querying with Credentials -[source,bash] ----- -$ curl -i -u user:8e557245-73e2-4286-969a-ff57fe326336 http://localhost:8080/some/path -HTTP/1.1 404 -... ----- - -then Spring Boot will service the request, returning a `404 Not Found` in this case since `/some/path` doesn't exist. - -From here, you can: - -* Better understand <> -* Read about <> that Spring Security helps with -* Start configuring xref:servlet/authentication/index.adoc[authentication] - -[[hello-expectations]] [[servlet-hello-auto-configuration]] -== Runtime Expectations +== Spring Boot Auto Configuration -The default arrangement of Spring Boot and Spring Security affords the following behaviors at runtime: +// FIXME: Link to relevant portions of documentation +// FIXME: Link to Spring Boot's Security Auto configuration classes +// FIXME: Add links for what user's should do next -* Requires an authenticated user xref:servlet/authorization/authorize-http-requests.adoc[for any endpoint] (including Boot's `/error` endpoint) -* xref:servlet/authentication/passwords/user-details-service.adoc[Registers a default user] with a generated password at startup (the password is logged to the console; in the preceding example, the password is `8e557245-73e2-4286-969a-ff57fe326336`) -* Protects xref:servlet/authentication/passwords/password-encoder.adoc[password storage with BCrypt] as well as others -* Provides form-based xref:servlet/authentication/passwords/form.adoc[login] and xref:servlet/authentication/logout.adoc[logout] flows -* Authenticates xref:servlet/authentication/passwords/form.adoc[form-based login] as well as xref:servlet/authentication/passwords/basic.adoc[HTTP Basic] -* Provides content negotiation; for web requests, redirects to the login page; for service requests, returns a `401 Unauthorized` -* xref:servlet/exploits/csrf.adoc[Mitigates CSRF] attacks -* xref:servlet/authentication/session-management.adoc#ns-session-fixation[Mitigates Session Fixation] attacks -* Writes xref:servlet/exploits/headers.adoc#servlet-headers-hsts[Strict-Transport-Security] to https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security[ensure HTTPS] -* Writes xref:servlet/exploits/headers.adoc#servlet-headers-content-type-options[X-Content-Type-Options] to mitigate https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-content-type-options[sniffing attacks] -* Writes xref:servlet/exploits/headers.adoc#servlet-headers-cache-control[Cache Control headers] that protect authenticated resources -* Writes xref:servlet/exploits/headers.adoc#servlet-headers-frame-options[X-Frame-Options] to mitigate https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-frame-options[Clickjacking] -* Integrates with xref:servlet/integrations/servlet-api.adoc[``HttpServletRequest``'s authentication methods] -* Publishes xref:servlet/authentication/events.adoc[authentication success and failure events] +Spring Boot automatically: -It can be helpful to understand how Spring Boot is coordinating with Spring Security to achieve this. -Taking a look at {spring-boot-api-url}org/springframework/boot/autoconfigure/security/servlet/SecurityAutoConfiguration.html[Boot's security auto configuration], it does the following (simplified for illustration): +* Enables Spring Security's default configuration, which creates a servlet `Filter` as a bean named `springSecurityFilterChain`. +This bean is responsible for all the security (protecting the application URLs, validating submitted username and passwords, redirecting to the login form, and so on) within your application. +* Creates a `UserDetailsService` bean with a username of `user` and a randomly generated password that is logged to the console. +* Registers the `Filter` with a bean named `springSecurityFilterChain` with the Servlet container for every request. -.Spring Boot Security Auto Configuration -[source,java] ----- -@EnableWebSecurity <1> -@Configuration -public class DefaultSecurityConfig { - @Bean - @ConditionalOnMissingBean(UserDetailsService.class) - InMemoryUserDetailsManager inMemoryUserDetailsManager() { <2> - String generatedPassword = // ...; - return new InMemoryUserDetailsManager(User.withUsername("user") - .password(generatedPassword).roles("ROLE_USER").build()); - } +Spring Boot is not configuring much, but it does a lot. +A summary of the features follows: - @Bean - @ConditionalOnMissingBean(AuthenticationEventPublisher.class) - DefaultAuthenticationEventPublisher defaultAuthenticationEventPublisher(ApplicationEventPublisher delegate) { <3> - return new DefaultAuthenticationEventPublisher(delegate); - } -} ----- -1. Adds the `@EnableWebSecurity` annotation. (Among other things, this publishes xref:servlet/architecture.adoc#servlet-securityfilterchain[Spring Security's default `Filter` chain] as a `@Bean`) -2. Publishes a xref:servlet/authentication/passwords/user-details-service.adoc[`UserDetailsService`] `@Bean` with a username of `user` and a randomly generated password that is logged to the console -3. Publishes an xref:servlet/authentication/events.adoc[`AuthenticationEventPublisher`] `@Bean` for publishing authentication events +* Require an authenticated user for any interaction with the application +* Generate a default login form for you +* Let the user with a username of `user` and a password that is logged to the console to authenticate with form-based authentication (in the preceding example, the password is `8e557245-73e2-4286-969a-ff57fe326336`) +* Protects the password storage with BCrypt +* Lets the user log out +* https://en.wikipedia.org/wiki/Cross-site_request_forgery[CSRF attack] prevention +* https://en.wikipedia.org/wiki/Session_fixation[Session Fixation] protection +* Security Header integration +** https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security[HTTP Strict Transport Security] for secure requests +** https://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx[X-Content-Type-Options] integration +** Cache Control (can be overridden later by your application to allow caching of your static resources) +** X-Frame-Options integration to help prevent https://en.wikipedia.org/wiki/Clickjacking[Clickjacking] +* Integrate with the following Servlet API methods: +** https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getRemoteUser()[`HttpServletRequest#getRemoteUser()`] +** https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getUserPrincipal()[`HttpServletRequest.html#getUserPrincipal()`] +** https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#isUserInRole(java.lang.String)[`HttpServletRequest.html#isUserInRole(java.lang.String)`] +** https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#login(java.lang.String,%20java.lang.String)[`HttpServletRequest.html#login(java.lang.String, java.lang.String)`] +** https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#logout()[`HttpServletRequest.html#logout()`] -[NOTE] -Spring Boot adds any `Filter` published as a `@Bean` to the application's filter chain. -This means that using `@EnableWebSecurity` in conjunction with Spring Boot automatically registers Spring Security's filter chain for every request. - -[[security-use-cases]] -== Security Use Cases - -There are a number of places that you may want to go from here. -To figure out what's next for you and your application, consider these common use cases that Spring Security is built to address: - -* I am building a REST API, and I need to xref:servlet/oauth2/resource-server/jwt.adoc[authenticate a JWT] or xref:servlet/oauth2/resource-server/opaque-token.adoc[other bearer token] -* I am building a Web Application, API Gateway, or BFF and -** I need to xref:servlet/oauth2/login/core.adoc[login using OAuth 2.0 or OIDC] -** I need to xref:servlet/saml2/login/index.adoc[login using SAML 2.0] -** I need to xref:servlet/authentication/cas.adoc[login using CAS] -* I need to manage -** Users in xref:servlet/authentication/passwords/ldap.adoc[LDAP] or xref:servlet/authentication/passwords/ldap.adoc#_active_directory[Active Directory], with xref:servlet/integrations/data.adoc[Spring Data], or with xref:servlet/authentication/passwords/jdbc.adoc[JDBC] -** xref:servlet/authentication/passwords/storage.adoc[Passwords] - -In case none of those match what you are looking for, consider thinking about your application in the following order: - -1. *Protocol*: First, consider the protocol your application will use to communicate. -For servlet-based applications, Spring Security supports HTTP as well as xref:servlet/integrations/websocket.adoc[Websockets]. -2. *Authentication*: Next, consider how users will xref:servlet/authentication/index.adoc[authenticate] and if that authentication will be stateful or stateless -3. *Authorization*: Then, consider how you will determine xref:servlet/authorization/index.adoc[what a user is authorized to do] -4. *Defense*: Finally, xref:servlet/exploits/csrf.adoc#csrf-considerations[integrate with Spring Security's default protections] and consider xref:servlet/exploits/headers.adoc[which additional protections you need] diff --git a/docs/modules/ROOT/pages/servlet/integrations/cors.adoc b/docs/modules/ROOT/pages/servlet/integrations/cors.adoc index 115e9b8f9e..b3e5199570 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/cors.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/cors.adoc @@ -6,8 +6,7 @@ CORS must be processed before Spring Security, because the pre-flight request do If the request does not contain any cookies and Spring Security is first, the request determines that the user is not authenticated (since there are no cookies in the request) and rejects it. The easiest way to ensure that CORS is handled first is to use the `CorsFilter`. -Users can integrate the `CorsFilter` with Spring Security by providing a `CorsConfigurationSource`. -For example, the following will integrate CORS support within Spring Security: +Users can integrate the `CorsFilter` with Spring Security by providing a `CorsConfigurationSource` that uses the following: [tabs] ====== @@ -15,14 +14,28 @@ Java:: + [source,java,role="primary"] ---- -@Bean -CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("https://example.com")); - configuration.setAllowedMethods(Arrays.asList("GET","POST")); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; +@Configuration +@EnableWebSecurity +public class WebSecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // by default uses a Bean by the name of corsConfigurationSource + .cors(withDefaults()) + ... + return http.build(); + } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("https://example.com")); + configuration.setAllowedMethods(Arrays.asList("GET","POST")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } } ---- @@ -30,14 +43,28 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -@Bean -fun corsConfigurationSource(): CorsConfigurationSource { - val configuration = CorsConfiguration() - configuration.allowedOrigins = listOf("https://example.com") - configuration.allowedMethods = listOf("GET", "POST") - val source = UrlBasedCorsConfigurationSource() - source.registerCorsConfiguration("/**", configuration) - return source +@Configuration +@EnableWebSecurity +open class WebSecurityConfig { + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + // by default uses a Bean by the name of corsConfigurationSource + cors { } + // ... + } + return http.build() + } + + @Bean + open fun corsConfigurationSource(): CorsConfigurationSource { + val configuration = CorsConfiguration() + configuration.allowedOrigins = listOf("https://example.com") + configuration.allowedMethods = listOf("GET", "POST") + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration("/**", configuration) + return source + } } ---- ====== @@ -110,76 +137,3 @@ The following listing does the same thing in XML: ... ---- - -If you have more than one `CorsConfigurationSource` bean, Spring Security won't automatically configure CORS support for you, that is because it cannot decide which one to use. -If you want to specify different `CorsConfigurationSource` for each `SecurityFilterChain`, you can pass it directly into the `.cors()` DSL. - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class WebSecurityConfig { - - @Bean - @Order(0) - public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { - http - .securityMatcher("/api/**") - .cors((cors) -> cors - .configurationSource(apiConfigurationSource()) - ) - ... - return http.build(); - } - - @Bean - @Order(1) - public SecurityFilterChain myOtherFilterChain(HttpSecurity http) throws Exception { - http - .cors((cors) -> cors - .configurationSource(myWebsiteConfigurationSource()) - ) - ... - return http.build(); - } - - CorsConfigurationSource apiConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("https://api.example.com")); - configuration.setAllowedMethods(Arrays.asList("GET","POST")); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; - } - - CorsConfigurationSource myWebsiteConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("https://example.com")); - configuration.setAllowedMethods(Arrays.asList("GET","POST")); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Bean -fun corsConfigurationSource(): CorsConfigurationSource { - val configuration = CorsConfiguration() - configuration.allowedOrigins = listOf("https://example.com") - configuration.allowedMethods = listOf("GET", "POST") - val source = UrlBasedCorsConfigurationSource() - source.registerCorsConfiguration("/**", configuration) - return source -} ----- -====== diff --git a/docs/modules/ROOT/pages/servlet/integrations/data.adoc b/docs/modules/ROOT/pages/servlet/integrations/data.adoc index 79a0efe53a..2143fd8cec 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/data.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/data.adoc @@ -71,4 +71,4 @@ interface MessageRepository : PagingAndSortingRepository { This checks to see if the `Authentication.getPrincipal().getId()` is equal to the recipient of the `Message`. Note that this example assumes you have customized the principal to be an Object that has an id property. -By exposing the `SecurityEvaluationContextExtension` bean, all of the xref:servlet/authorization/method-security.adoc#authorization-expressions[Common Security Expressions] are available within the Query. +By exposing the `SecurityEvaluationContextExtension` bean, all of the xref:servlet/authorization/expression-based.adoc#common-expressions[Common Security Expressions] are available within the Query. diff --git a/docs/modules/ROOT/pages/servlet/integrations/jsp-taglibs.adoc b/docs/modules/ROOT/pages/servlet/integrations/jsp-taglibs.adoc index e5c8aed6c5..4cd54572c8 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/jsp-taglibs.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/jsp-taglibs.adoc @@ -21,7 +21,7 @@ In Spring Security 3.0, it can be used in two ways. The legacy options from Spring Security 2.0 are also supported, but discouraged. ==== -The first approach uses a xref:servlet/authorization/authorize-http-requests.adoc#authorization-expressions[web-security expression], which is specified in the `access` attribute of the tag. +The first approach uses a xref:servlet/authorization/expression-based.adoc#el-access-web[web-security expression], which is specified in the `access` attribute of the tag. The expression evaluation is delegated to the `SecurityExpressionHandler` defined in the application context (you should have web expressions enabled in your `` namespace configuration to make sure this service is available). So, for example, you might have: diff --git a/docs/modules/ROOT/pages/servlet/integrations/observability.adoc b/docs/modules/ROOT/pages/servlet/integrations/observability.adoc index 3adf215d5b..32f209c8a8 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/observability.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/observability.adoc @@ -203,7 +203,7 @@ Java:: @Bean ObservationRegistryCustomizer noSpringSecurityObservations() { ObservationPredicate predicate = (name, context) -> !name.startsWith("spring.security."); - return (registry) -> registry.observationConfig().observationPredicate(predicate); + return (registry) -> registry.observationConfig().observationPredicate(predicate) } ---- diff --git a/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc b/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc index 048f9b0b55..2fe025c97c 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc @@ -27,7 +27,7 @@ Spring Security 4.0 has introduced authorization support for WebSockets through In Spring Security 5.8, this support has been refreshed to use the `AuthorizationManager` API. -To configure authorization using Java Configuration, simply include the `@EnableWebSocketSecurity` annotation and publish an `AuthorizationManager>` bean or in xref:servlet/appendix/namespace/websocket.adoc#nsa-websocket-security[XML] use the `use-authorization-manager` attribute. +To configure authorization using Java Configuration, simply include the `@EnableWebSocketSecurity` annotation and publish an `AuthorizationManager>` bean or in XML use the `use-authorization-manager` attribute. One way to do this is by using the `AuthorizationManagerMessageMatcherRegistry` to specify endpoint patterns like so: [tabs] @@ -43,7 +43,7 @@ public class WebSocketSecurityConfig { @Bean AuthorizationManager> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { messages - .simpDestMatchers("/user/**").hasRole("USER") // <3> + .simpDestMatchers("/user/**").authenticated() // <3> return messages.build(); } @@ -58,25 +58,33 @@ Kotlin:: @EnableWebSocketSecurity // <1> <2> open class WebSocketSecurityConfig { // <1> <2> @Bean - fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager> { - messages.simpDestMatchers("/user/**").hasRole("USER") // <3> + fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager> { + messages.simpDestMatchers("/user/**").authenticated() // <3> return messages.build() } } ---- - -Xml:: -+ -[source,xml,role="secondary"] ----- - <1> <2> - <3> - ----- ====== <1> Any inbound CONNECT message requires a valid CSRF token to enforce the <>. <2> The `SecurityContextHolder` is populated with the user within the `simpUser` header attribute for any inbound request. -<3> Our messages require the proper authorization. Specifically, any inbound message that starts with `/user/` will require `ROLE_USER`. You can find additional details on authorization in <> +<3> Our messages require the proper authorization. Specifically, any inbound message that starts with `/user/` will requires `ROLE_USER`. You can find additional details on authorization in <> + +Spring Security also provides xref:servlet/appendix/namespace/websocket.adoc#nsa-websocket-security[XML Namespace] support for securing WebSockets. +A comparable XML based configuration looks like the following: + +[source,xml] +---- + + + +---- + +This will ensure that: + +<1> Any inbound CONNECT message requires a valid CSRF token to enforce <> +<2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request. +<3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <> + === Custom Authorization @@ -108,7 +116,7 @@ Kotlin:: @EnableWebSocketSecurity // <1> <2> open class WebSocketSecurityConfig { @Bean - fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager> { + fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager> { return AuthorityAuthorizationManager.hasRole("USER") // <3> } } @@ -156,7 +164,7 @@ Kotlin:: ---- @Configuration open class WebSocketSecurityConfig { - fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager> { + fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager { messages .nullDestMatcher().authenticated() // <1> .simpSubscribeDestMatchers("/user/queue/errors").permitAll() // <2> @@ -394,7 +402,7 @@ open class WebSocketSecurityConfig : WebSocketMessageBrokerConfigurer { @Override override fun configureClientInboundChannel(registration: ChannelRegistration) { - var myAuthorizationRules: AuthorizationManager> = AuthenticatedAuthorizationManager.authenticated() + var myAuthorizationRules: AuthorizationManager> = AuthenticatedAuthorizationManager.authenticated() var authz: AuthorizationChannelInterceptor = AuthorizationChannelInterceptor(myAuthorizationRules) var publisher: AuthorizationEventPublisher = SpringAuthorizationEventPublisher(this.context) authz.setAuthorizationEventPublisher(publisher) diff --git a/docs/modules/ROOT/pages/servlet/oauth2/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/index.adoc index 512400577d..abcb2591d8 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/index.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/index.adoc @@ -1,1687 +1,7 @@ = OAuth2 +:page-section-summary-toc: 1 -Spring Security provides comprehensive OAuth 2.0 support. -This section discusses how to integrate OAuth 2.0 into your servlet based application. +Spring Security provides comprehensive OAuth 2 support. +This section discusses how to integrate OAuth 2 into your servlet based application. -[[oauth2-overview]] -== Overview -Spring Security's OAuth 2.0 support consists of two primary feature sets: - -* <> -* <> - -[NOTE] -==== -<> is a very powerful OAuth2 Client feature that deserves its own section in the reference documentation. -However, it does not exist as a standalone feature and requires OAuth2 Client in order to function. -==== - -These feature sets cover the _resource server_ and _client_ roles defined in the https://tools.ietf.org/html/rfc6749#section-1.1[OAuth 2.0 Authorization Framework], while the _authorization server_ role is covered by https://docs.spring.io/spring-authorization-server/reference/index.html[Spring Authorization Server], which is a separate project built on xref:index.adoc[Spring Security]. - -The _resource server_ and _client_ roles in OAuth2 are typically represented by one or more server-side applications. -Additionally, the _authorization server_ role can be represented by one or more third parties (as is the case when centralizing identity management and/or authentication within an organization) *-or-* it can be represented by an application (as is the case with Spring Authorization Server). - -For example, a typical OAuth2-based microservices architecture might consist of a single user-facing client application, several backend resource servers providing REST APIs and a third party authorization server for managing users and authentication concerns. -It is also common to have a single application representing only one of these roles with the need to integrate with one or more third parties that are providing the other roles. - -Spring Security handles these scenarios and more. -The following sections cover the roles provided by Spring Security and contain examples for common scenarios. - -[[oauth2-resource-server]] -== OAuth2 Resource Server - -[NOTE] -==== -This section contains a summary of OAuth2 Resource Server features with examples. -See xref:servlet/oauth2/resource-server/index.adoc[OAuth 2.0 Resource Server] for complete reference documentation. -==== - -To get started, add the `spring-security-oauth2-resource-server` dependency to your project. -When using Spring Boot, add the following starter: - -.OAuth2 Client with Spring Boot -[tabs] -====== -Gradle:: -+ -[source,gradle,role="primary"] ----- -implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' ----- - -Maven:: -+ -[source,maven,role="secondary"] ----- - - org.springframework.boot - spring-boot-starter-oauth2-resource-server - ----- -====== - -[TIP] -==== -See xref:getting-spring-security.adoc[] for additional options when not using Spring Boot. -==== - -Consider the following use cases for OAuth2 Resource Server: - -* <> (authorization server provides JWT or opaque access token) -* <> (custom token) - -[[oauth2-resource-server-access-token]] -=== Protect Access with an OAuth2 Access Token - -It is very common to protect access to an API using OAuth2 access tokens. -In most cases, Spring Security requires only minimal configuration to secure an application with OAuth2. - -There are two types of `Bearer` tokens supported by Spring Security which each use a different component for validation: - -* <> uses a `JwtDecoder` bean to validate signatures and decode tokens -* <> uses an `OpaqueTokenIntrospector` bean to introspect tokens - -[[oauth2-resource-server-access-token-jwt]] -==== JWT Support - -The following example configures a `JwtDecoder` bean using Spring Boot configuration properties: - -[source,yaml] ----- -spring: - security: - oauth2: - resourceserver: - jwt: - issuer-uri: https://my-auth-server.com ----- - -When using Spring Boot, this is all that is required. -The default arrangement provided by Spring Boot is equivalent to the following: - -.Configure Resource Server with JWTs -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests((authorize) -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer((oauth2) -> oauth2 - .jwt(Customizer.withDefaults()) - ); - return http.build(); - } - - @Bean - public JwtDecoder jwtDecoder() { - return JwtDecoders.fromIssuerLocation("https://my-auth-server.com"); - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -import org.springframework.security.config.annotation.web.invoke - -@Configuration -@EnableWebSecurity -class SecurityConfig { - - @Bean - fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - authorizeHttpRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - jwt { } - } - } - - return http.build() - } - - @Bean - fun jwtDecoder(): JwtDecoder { - return JwtDecoders.fromIssuerLocation("https://my-auth-server.com") - } - -} ----- -===== - -[[oauth2-resource-server-access-token-opaque]] -==== Opaque Token Support - -The following example configures an `OpaqueTokenIntrospector` bean using Spring Boot configuration properties: - -[source,yaml] ----- -spring: - security: - oauth2: - resourceserver: - opaquetoken: - introspection-uri: https://my-auth-server.com/oauth2/introspect - client-id: my-client-id - client-secret: my-client-secret ----- - -When using Spring Boot, this is all that is required. -The default arrangement provided by Spring Boot is equivalent to the following: - -.Configure Resource Server with Opaque Tokens -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests((authorize) -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer((oauth2) -> oauth2 - .opaqueToken(Customizer.withDefaults()) - ); - return http.build(); - } - - @Bean - public OpaqueTokenIntrospector opaqueTokenIntrospector() { - return new SpringOpaqueTokenIntrospector( - "https://my-auth-server.com/oauth2/introspect", "my-client-id", "my-client-secret"); - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -import org.springframework.security.config.annotation.web.invoke - -@Configuration -@EnableWebSecurity -class SecurityConfig { - - @Bean - fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - authorizeHttpRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - opaqueToken { } - } - } - - return http.build() - } - - @Bean - fun opaqueTokenIntrospector(): OpaqueTokenIntrospector { - return SpringOpaqueTokenIntrospector( - "https://my-auth-server.com/oauth2/introspect", "my-client-id", "my-client-secret" - ) - } - -} ----- -===== - -[[oauth2-resource-server-custom-jwt]] -=== Protect Access with a custom JWT - -It is a fairly common goal to protect access to an API using JWTs, particularly when the frontend is developed as a single-page application. -The OAuth2 Resource Server support in Spring Security can be used for any type of `Bearer` token, including a custom JWT. - -All that is required to protect an API using JWTs is a `JwtDecoder` bean, which is used to validate signatures and decode tokens. -Spring Security will automatically use the provided bean to configure protection within the `SecurityFilterChain`. - -The following example configures a `JwtDecoder` bean using Spring Boot configuration properties: - -[source,yaml] ----- -spring: - security: - oauth2: - resourceserver: - jwt: - public-key-location: classpath:my-public-key.pub ----- - -[NOTE] -==== -You can provide the public key as a classpath resource (called `my-public-key.pub` in this example). -==== - -When using Spring Boot, this is all that is required. -The default arrangement provided by Spring Boot is equivalent to the following: - -.Configure Resource Server with Custom JWTs -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests((authorize) -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer((oauth2) -> oauth2 - .jwt(Customizer.withDefaults()) - ); - return http.build(); - } - - @Bean - public JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withPublicKey(publicKey()).build(); - } - - private RSAPublicKey publicKey() { - // ... - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -import org.springframework.security.config.annotation.web.invoke - -@Configuration -@EnableWebSecurity -class SecurityConfig { - - @Bean - fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - authorizeHttpRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - jwt { } - } - } - - return http.build() - } - - @Bean - fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withPublicKey(publicKey()).build() - } - - private fun publicKey(): RSAPublicKey { - // ... - } - -} ----- -===== - -[NOTE] -==== -Spring Security does not provide an endpoint for minting tokens. -However, Spring Security does provide the `JwtEncoder` interface along with one implementation, which is `NimbusJwtEncoder`. -==== - -[[oauth2-client]] -== OAuth2 Client - -[NOTE] -==== -This section contains a summary of OAuth2 Client features with examples. -See xref:servlet/oauth2/client/index.adoc[OAuth 2.0 Client] and xref:servlet/oauth2/login/index.adoc[OAuth 2.0 Login] for complete reference documentation. -==== - -To get started, add the `spring-security-oauth2-client` dependency to your project. -When using Spring Boot, add the following starter: - -.OAuth2 Client with Spring Boot -[tabs] -====== -Gradle:: -+ -[source,gradle,role="primary"] ----- -implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' ----- - -Maven:: -+ -[source,maven,role="secondary"] ----- - - org.springframework.boot - spring-boot-starter-oauth2-client - ----- -====== - -[TIP] -==== -See xref:getting-spring-security.adoc[] for additional options when not using Spring Boot. -==== - -Consider the following use cases for OAuth2 Client: - -* <> -* <> -* <> (log users in _and_ access a third-party API) -* <> -* <> -* <> -* <> - -[[oauth2-client-log-users-in]] -=== Log Users In with OAuth2 - -It is very common to require users to log in via OAuth2. -https://openid.net/specs/openid-connect-core-1_0.html[OpenID Connect 1.0] provides a special token called the `id_token` which is designed to provide an OAuth2 Client with the ability to perform user identity verification and log users in. -In certain cases, OAuth2 can be used directly to log users in (as is the case with popular social login providers that do not implement OpenID Connect such as GitHub and Facebook). - -The following example configures the application to act as an OAuth2 Client capable of logging users in with OAuth2 or OpenID Connect: - -.Configure OAuth2 Login -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - // ... - .oauth2Login(Customizer.withDefaults()); - return http.build(); - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -import org.springframework.security.config.annotation.web.invoke - -@Configuration -@EnableWebSecurity -class SecurityConfig { - - @Bean - fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - // ... - oauth2Login { } - } - - return http.build() - } - -} ----- -===== - -In addition to the above configuration, the application requires at least one `ClientRegistration` to be configured through the use of a `ClientRegistrationRepository` bean. -The following example configures an `InMemoryClientRegistrationRepository` bean using Spring Boot configuration properties: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - my-oidc-client: - provider: my-oidc-provider - client-id: my-client-id - client-secret: my-client-secret - authorization-grant-type: authorization_code - scope: openid,profile - provider: - my-oidc-provider: - issuer-uri: https://my-oidc-provider.com ----- - -With the above configuration, the application now supports two additional endpoints: - -1. The login endpoint (e.g. `/oauth2/authorization/my-oidc-client`) is used to initiate login and perform a redirect to the third party authorization server. -2. The redirection endpoint (e.g. `/login/oauth2/code/my-oidc-client`) is used by the authorization server to redirect back to the client application, and will contain a `code` parameter used to obtain an `id_token` and/or `access_token` via the access token request. - -[NOTE] -==== -The presence of the `openid` scope in the above configuration indicates that OpenID Connect 1.0 should be used. -This instructs Spring Security to use OIDC-specific components (such as `OidcUserService`) during request processing. -Without this scope, Spring Security will use OAuth2-specific components (such as `OAuth2UserService`) instead. -==== - -[[oauth2-client-access-protected-resources]] -=== Access Protected Resources - -Making requests to a third party API that is protected by OAuth2 is a core use case of OAuth2 Client. -This is accomplished by authorizing a client (represented by the `OAuth2AuthorizedClient` class in Spring Security) and accessing protected resources by placing a `Bearer` token in the `Authorization` header of an outbound request. - -The following example configures the application to act as an OAuth2 Client capable of requesting protected resources from a third party API: - -.Configure OAuth2 Client -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - // ... - .oauth2Client(Customizer.withDefaults()); - return http.build(); - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -import org.springframework.security.config.annotation.web.invoke - -@Configuration -@EnableWebSecurity -class SecurityConfig { - - @Bean - fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - // ... - oauth2Client { } - } - - return http.build() - } - -} ----- -===== - -[NOTE] -==== -The above example does not provide a way to log users in. -You can use any other login mechanism (such as `formLogin()`). -See the <> for an example combining `oauth2Client()` with `oauth2Login()`. -==== - -In addition to the above configuration, the application requires at least one `ClientRegistration` to be configured through the use of a `ClientRegistrationRepository` bean. -The following example configures an `InMemoryClientRegistrationRepository` bean using Spring Boot configuration properties: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - my-oauth2-client: - provider: my-auth-server - client-id: my-client-id - client-secret: my-client-secret - authorization-grant-type: authorization_code - scope: message.read,message.write - provider: - my-auth-server: - issuer-uri: https://my-auth-server.com ----- - -In addition to configuring Spring Security to support OAuth2 Client features, you will also need to decide how you will be accessing protected resources and configure your application accordingly. -Spring Security provides implementations of `OAuth2AuthorizedClientManager` for obtaining access tokens that can be used to access protected resources. - -[TIP] -==== -Spring Security registers a default `OAuth2AuthorizedClientManager` bean for you when one does not exist. -==== - -The easiest way to use an `OAuth2AuthorizedClientManager` is via an `ExchangeFilterFunction` that intercepts requests through a `WebClient`. -To use `WebClient`, you will need to add the `spring-webflux` dependency along with a reactive client implementation: - -.Add Spring WebFlux Dependency -[tabs] -====== -Gradle:: -+ -[source,gradle,role="primary"] ----- -implementation 'org.springframework:spring-webflux' -implementation 'io.projectreactor.netty:reactor-netty' ----- - -Maven:: -+ -[source,maven,role="secondary"] ----- - - org.springframework - spring-webflux - - - io.projectreactor.netty - reactor-netty - ----- -====== - -The following example uses the default `OAuth2AuthorizedClientManager` to configure a `WebClient` capable of accessing protected resources by placing `Bearer` tokens in the `Authorization` header of each request: - -.Configure `WebClient` with `ExchangeFilterFunction` -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction filter = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - return WebClient.builder() - .apply(filter.oauth2Configuration()) - .build(); - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -class WebClientConfig { - - @Bean - fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager): WebClient { - val filter = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - return WebClient.builder() - .apply(filter.oauth2Configuration()) - .build() - } - -} ----- -===== - -This configured `WebClient` can be used as in the following example: - -[[oauth2-client-accessing-protected-resources-example]] -.Use `WebClient` to Access Protected Resources -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId; - -@RestController -public class MessagesController { - - private final WebClient webClient; - - public MessagesController(WebClient webClient) { - this.webClient = webClient; - } - - @GetMapping("/messages") - public ResponseEntity> messages() { - return this.webClient.get() - .uri("http://localhost:8090/messages") - .attributes(clientRegistrationId("my-oauth2-client")) - .retrieve() - .toEntityList(Message.class) - .block(); - } - - public record Message(String message) { - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId - -@RestController -class MessagesController(private val webClient: WebClient) { - - @GetMapping("/messages") - fun messages(): ResponseEntity> { - return webClient.get() - .uri("http://localhost:8090/messages") - .attributes(clientRegistrationId("my-oauth2-client")) - .retrieve() - .toEntityList(Message::class.java) - .block()!! - } - - data class Message(val message: String) - -} ----- -===== - -[[oauth2-client-access-protected-resources-current-user]] -=== Access Protected Resources for the Current User - -When a user is logged in via OAuth2 or OpenID Connect, the authorization server may provide an access token that can be used directly to access protected resources. -This is convenient because it only requires a single `ClientRegistration` to be configured for both use cases simultaneously. - -[NOTE] -==== -This section combines <> and <> into a single configuration. -Other advanced scenarios exist, such as configuring one `ClientRegistration` for login and another for accessing protected resources. -All such scenarios would use the same basic configuration. -==== - -The following example configures the application to act as an OAuth2 Client capable of logging the user in _and_ requesting protected resources from a third party API: - -.Configure OAuth2 Login and OAuth2 Client -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - // ... - .oauth2Login(Customizer.withDefaults()) - .oauth2Client(Customizer.withDefaults()); - return http.build(); - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -import org.springframework.security.config.annotation.web.invoke - -@Configuration -@EnableWebSecurity -class SecurityConfig { - - @Bean - fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - // ... - oauth2Login { } - oauth2Client { } - } - - return http.build() - } - -} ----- -===== - -In addition to the above configuration, the application requires at least one `ClientRegistration` to be configured through the use of a `ClientRegistrationRepository` bean. -The following example configures an `InMemoryClientRegistrationRepository` bean using Spring Boot configuration properties: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - my-combined-client: - provider: my-auth-server - client-id: my-client-id - client-secret: my-client-secret - authorization-grant-type: authorization_code - scope: openid,profile,message.read,message.write - provider: - my-auth-server: - issuer-uri: https://my-auth-server.com ----- - -[NOTE] -==== -The main difference between the previous examples (<>, <>) and this one is what is configured via the `scope` property, which combines the standard scopes `openid` and `profile` with the custom scopes `message.read` and `message.write`. -==== - -In addition to configuring Spring Security to support OAuth2 Client features, you will also need to decide how you will be accessing protected resources and configure your application accordingly. -Spring Security provides implementations of `OAuth2AuthorizedClientManager` for obtaining access tokens that can be used to access protected resources. - -[TIP] -==== -Spring Security registers a default `OAuth2AuthorizedClientManager` bean for you when one does not exist. -==== - -The easiest way to use an `OAuth2AuthorizedClientManager` is via an `ExchangeFilterFunction` that intercepts requests through a `WebClient`. -To use `WebClient`, you will need to add the `spring-webflux` dependency along with a reactive client implementation: - -.Add Spring WebFlux Dependency -[tabs] -====== -Gradle:: -+ -[source,gradle,role="primary"] ----- -implementation 'org.springframework:spring-webflux' -implementation 'io.projectreactor.netty:reactor-netty' ----- - -Maven:: -+ -[source,maven,role="secondary"] ----- - - org.springframework - spring-webflux - - - io.projectreactor.netty - reactor-netty - ----- -====== - -The following example uses the default `OAuth2AuthorizedClientManager` to configure a `WebClient` capable of accessing protected resources by placing `Bearer` tokens in the `Authorization` header of each request: - -.Configure `WebClient` with `ExchangeFilterFunction` -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction filter = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - return WebClient.builder() - .apply(filter.oauth2Configuration()) - .build(); - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -class WebClientConfig { - - @Bean - fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager): WebClient { - val filter = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - return WebClient.builder() - .apply(filter.oauth2Configuration()) - .build() - } - -} ----- -===== - -This configured `WebClient` can be used as in the following example: - -[[oauth2-client-accessing-protected-resources-current-user-example]] -.Use `WebClient` to Access Protected Resources (Current User) -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@RestController -public class MessagesController { - - private final WebClient webClient; - - public MessagesController(WebClient webClient) { - this.webClient = webClient; - } - - @GetMapping("/messages") - public ResponseEntity> messages() { - return this.webClient.get() - .uri("http://localhost:8090/messages") - .retrieve() - .toEntityList(Message.class) - .block(); - } - - public record Message(String message) { - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@RestController -class MessagesController(private val webClient: WebClient) { - - @GetMapping("/messages") - fun messages(): ResponseEntity> { - return webClient.get() - .uri("http://localhost:8090/messages") - .retrieve() - .toEntityList(Message::class.java) - .block()!! - } - - data class Message(val message: String) - -} ----- -===== - -[NOTE] -==== -Unlike the <>, notice that we do not need to tell Spring Security about the `clientRegistrationId` we'd like to use. -This is because it can be derived from the currently logged in user. -==== - -[[oauth2-client-enable-extension-grant-type]] -=== Enable an Extension Grant Type - -A common use case involves enabling and/or configuring an extension grant type. -For example, Spring Security provides support for the `jwt-bearer` grant type, but does not enable it by default because it is not part of the core OAuth 2.0 specification. - -With Spring Security 6.2 and later, we can simply publish a bean for one or more `OAuth2AuthorizedClientProvider` and they will be picked up automatically. -The following example simply enables the `jwt-bearer` grant type: - -.Enable `jwt-bearer` Grant Type -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -public class SecurityConfig { - - @Bean - public OAuth2AuthorizedClientProvider jwtBearer() { - return new JwtBearerOAuth2AuthorizedClientProvider(); - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -class SecurityConfig { - - @Bean - fun jwtBearer(): OAuth2AuthorizedClientProvider { - return JwtBearerOAuth2AuthorizedClientProvider() - } - -} ----- -===== - -A default `OAuth2AuthorizedClientManager` will be published automatically by Spring Security when one is not already provided. - -[TIP] -==== -Any custom `OAuth2AuthorizedClientProvider` bean will also be picked up and applied to the provided `OAuth2AuthorizedClientManager` after the default grant types. -==== - -In order to achieve the above configuration prior to Spring Security 6.2, we had to publish this bean ourselves and ensure we re-enabled default grant types as well. -To understand what is being configured behind the scenes, here's what the configuration might have looked like: - -.Enable `jwt-bearer` Grant Type (prior to 6.2) -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -public class SecurityConfig { - - @Bean - public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .provider(new JwtBearerOAuth2AuthorizedClientProvider()) - .build(); - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -class SecurityConfig { - - @Bean - fun authorizedClientManager( - clientRegistrationRepository: ClientRegistrationRepository, - authorizedClientRepository: OAuth2AuthorizedClientRepository - ): OAuth2AuthorizedClientManager { - val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .provider(JwtBearerOAuth2AuthorizedClientProvider()) - .build() - - val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository - ) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - - return authorizedClientManager - } - -} ----- -===== - -[[oauth2-client-customize-existing-grant-type]] -=== Customize an Existing Grant Type - -The ability to <> by publishing a bean also provides the opportunity for customizing an existing grant type without the need to re-define the defaults. -For example, if we want to customize the clock skew of the `OAuth2AuthorizedClientProvider` for the `client_credentials` grant, we can simply publish a bean like so: - -.Customize Client Credentials Grant Type -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -public class SecurityConfig { - - @Bean - public OAuth2AuthorizedClientProvider clientCredentials() { - ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = - new ClientCredentialsOAuth2AuthorizedClientProvider(); - authorizedClientProvider.setClockSkew(Duration.ofMinutes(5)); - - return authorizedClientProvider; - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -class SecurityConfig { - - @Bean - fun clientCredentials(): OAuth2AuthorizedClientProvider { - val authorizedClientProvider = ClientCredentialsOAuth2AuthorizedClientProvider() - authorizedClientProvider.setClockSkew(Duration.ofMinutes(5)) - return authorizedClientProvider - } - -} ----- -===== - -[[oauth2-client-customize-request-parameters]] -=== Customize Token Request Parameters - -The need to customize request parameters when obtaining an access token is fairly common. -For example, let's say we want to add a custom `audience` parameter to the token request because the provider requires this parameter for the `authorization_code` grant. - -With Spring Security 6.2 and later, we can simply publish a bean of type `OAuth2AccessTokenResponseClient` with the generic type `OAuth2AuthorizationCodeGrantRequest` and it will be used by Spring Security to configure OAuth2 Client components. - -The following example customizes token request parameters for the `authorization_code` grant without the DSL: - -.Customize Token Request Parameters for Authorization Code Grant -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -public class SecurityConfig { - - @Bean - public OAuth2AccessTokenResponseClient authorizationCodeAccessTokenResponseClient() { - OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = - new OAuth2AuthorizationCodeGrantRequestEntityConverter(); - requestEntityConverter.addParametersConverter(parametersConverter()); - - DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = - new DefaultAuthorizationCodeTokenResponseClient(); - accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter); - - return accessTokenResponseClient; - } - - private static Converter> parametersConverter() { - return (grantRequest) -> { - MultiValueMap parameters = new LinkedMultiValueMap<>(); - parameters.set("audience", "xyz_value"); - - return parameters; - }; - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -class SecurityConfig { - - @Bean - fun authorizationCodeAccessTokenResponseClient(): OAuth2AccessTokenResponseClient { - val requestEntityConverter = OAuth2AuthorizationCodeGrantRequestEntityConverter() - requestEntityConverter.addParametersConverter(parametersConverter()) - - val accessTokenResponseClient = DefaultAuthorizationCodeTokenResponseClient() - accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter) - - return accessTokenResponseClient - } - - private fun parametersConverter(): Converter> { - return Converter> { grantRequest -> - LinkedMultiValueMap().also { parameters -> - parameters["audience"] = "xyz_value" - } - } - } - -} ----- -===== - -[TIP] -==== -Notice that we don't need to customize the `SecurityFilterChain` bean in this case, and can stick with the defaults. -If using Spring Boot with no additional customizations, we can actually omit the `SecurityFilterChain` bean entirely. -==== - -Prior to Spring Security 6.2, we had to ensure that this customization was applied for both OAuth2 Login (if we are using this feature) and OAuth2 Client components using the Spring Security DSL. -To understand what is being configured behind the scenes, here's what the configuration might have looked like: - -.Customize Token Request Parameters for Authorization Code Grant (prior to 6.2) -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = - new OAuth2AuthorizationCodeGrantRequestEntityConverter(); - requestEntityConverter.addParametersConverter(parametersConverter()); - - DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = - new DefaultAuthorizationCodeTokenResponseClient(); - accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter); - - http - .authorizeHttpRequests((authorize) -> authorize - .anyRequest().authenticated() - ) - .oauth2Login((oauth2Login) -> oauth2Login - .tokenEndpoint((tokenEndpoint) -> tokenEndpoint - .accessTokenResponseClient(accessTokenResponseClient) - ) - ) - .oauth2Client((oauth2Client) -> oauth2Client - .authorizationCodeGrant((authorizationCode) -> authorizationCode - .accessTokenResponseClient(accessTokenResponseClient) - ) - ); - - return http.build(); - } - - private static Converter> parametersConverter() { - // ... - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -import org.springframework.security.config.annotation.web.invoke - -@Configuration -@EnableWebSecurity -class SecurityConfig { - - @Bean - fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - val requestEntityConverter = OAuth2AuthorizationCodeGrantRequestEntityConverter() - requestEntityConverter.addParametersConverter(parametersConverter()) - - val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient() - tokenResponseClient.setRequestEntityConverter(requestEntityConverter) - - http { - authorizeHttpRequests { - authorize(anyRequest, authenticated) - } - oauth2Login { - tokenEndpoint { - accessTokenResponseClient = tokenResponseClient - } - } - oauth2Client { - authorizationCodeGrant { - accessTokenResponseClient = tokenResponseClient - } - } - } - - return http.build() - } - - private fun parametersConverter(): Converter> { - // ... - } - -} ----- -===== - -For other grant types we can publish additional `OAuth2AccessTokenResponseClient` beans to override the defaults. -For example, to customize token requests for the `client_credentials` grant we can publish the following bean: - -.Customize Token Request Parameters for Client Credentials Grant -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -public class SecurityConfig { - - @Bean - public OAuth2AccessTokenResponseClient clientCredentialsAccessTokenResponseClient() { - OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter = - new OAuth2ClientCredentialsGrantRequestEntityConverter(); - requestEntityConverter.addParametersConverter(parametersConverter()); - - DefaultClientCredentialsTokenResponseClient accessTokenResponseClient = - new DefaultClientCredentialsTokenResponseClient(); - accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter); - - return accessTokenResponseClient; - } - - private static Converter> parametersConverter() { - // ... - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -class SecurityConfig { - - @Bean - fun clientCredentialsAccessTokenResponseClient(): OAuth2AccessTokenResponseClient { - val requestEntityConverter = OAuth2ClientCredentialsGrantRequestEntityConverter() - requestEntityConverter.addParametersConverter(parametersConverter()) - - val accessTokenResponseClient = DefaultClientCredentialsTokenResponseClient() - accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter) - - return accessTokenResponseClient - } - - private fun parametersConverter(): Converter> { - // ... - } - -} ----- -===== - -Spring Security automatically resolves the following generic types of `OAuth2AccessTokenResponseClient` beans: - -* `OAuth2AuthorizationCodeGrantRequest` (see `DefaultAuthorizationCodeTokenResponseClient`) -* `OAuth2RefreshTokenGrantRequest` (see `DefaultRefreshTokenTokenResponseClient`) -* `OAuth2ClientCredentialsGrantRequest` (see `DefaultClientCredentialsTokenResponseClient`) -* `OAuth2PasswordGrantRequest` (see `DefaultPasswordTokenResponseClient`) -* `JwtBearerGrantRequest` (see `DefaultJwtBearerTokenResponseClient`) - -[TIP] -==== -Publishing a bean of type `OAuth2AccessTokenResponseClient` will automatically enable the `jwt-bearer` grant type without the need to <>. -==== - -[[oauth2-client-customize-rest-operations]] -=== Customize the `RestOperations` used by OAuth2 Client Components - -Another common use case is the need to customize the `RestOperations` used when obtaining an access token. -We might need to do this to customize processing of the response (via a custom `HttpMessageConverter`) or to apply proxy settings for a corporate network (via a customized `ClientHttpRequestFactory`). - -With Spring Security 6.2 and later, we can simply publish beans of type `OAuth2AccessTokenResponseClient` and Spring Security will configure and publish an `OAuth2AuthorizedClientManager` bean for us. - -The following example customizes the `RestOperations` for all of the supported grant types: - -.Customize `RestOperations` for OAuth2 Client -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -public class SecurityConfig { - - @Bean - public OAuth2AccessTokenResponseClient authorizationCodeAccessTokenResponseClient() { - DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = - new DefaultAuthorizationCodeTokenResponseClient(); - accessTokenResponseClient.setRestOperations(restTemplate()); - - return accessTokenResponseClient; - } - - @Bean - public OAuth2AccessTokenResponseClient refreshTokenAccessTokenResponseClient() { - DefaultRefreshTokenTokenResponseClient accessTokenResponseClient = - new DefaultRefreshTokenTokenResponseClient(); - accessTokenResponseClient.setRestOperations(restTemplate()); - - return accessTokenResponseClient; - } - - @Bean - public OAuth2AccessTokenResponseClient clientCredentialsAccessTokenResponseClient() { - DefaultClientCredentialsTokenResponseClient accessTokenResponseClient = - new DefaultClientCredentialsTokenResponseClient(); - accessTokenResponseClient.setRestOperations(restTemplate()); - - return accessTokenResponseClient; - } - - @Bean - public OAuth2AccessTokenResponseClient passwordAccessTokenResponseClient() { - DefaultPasswordTokenResponseClient accessTokenResponseClient = - new DefaultPasswordTokenResponseClient(); - accessTokenResponseClient.setRestOperations(restTemplate()); - - return accessTokenResponseClient; - } - - @Bean - public OAuth2AccessTokenResponseClient jwtBearerAccessTokenResponseClient() { - DefaultJwtBearerTokenResponseClient accessTokenResponseClient = - new DefaultJwtBearerTokenResponseClient(); - accessTokenResponseClient.setRestOperations(restTemplate()); - - return accessTokenResponseClient; - } - - @Bean - public RestTemplate restTemplate() { - // ... - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -class SecurityConfig { - - @Bean - fun authorizationCodeAccessTokenResponseClient(): OAuth2AccessTokenResponseClient { - val accessTokenResponseClient = DefaultAuthorizationCodeTokenResponseClient() - accessTokenResponseClient.setRestOperations(restTemplate()) - - return accessTokenResponseClient - } - - @Bean - fun refreshTokenAccessTokenResponseClient(): OAuth2AccessTokenResponseClient { - val accessTokenResponseClient = DefaultRefreshTokenTokenResponseClient() - accessTokenResponseClient.setRestOperations(restTemplate()) - - return accessTokenResponseClient - } - - @Bean - fun clientCredentialsAccessTokenResponseClient(): OAuth2AccessTokenResponseClient { - val accessTokenResponseClient = DefaultClientCredentialsTokenResponseClient() - accessTokenResponseClient.setRestOperations(restTemplate()) - - return accessTokenResponseClient - } - - @Bean - fun passwordAccessTokenResponseClient(): OAuth2AccessTokenResponseClient { - val accessTokenResponseClient = DefaultPasswordTokenResponseClient() - accessTokenResponseClient.setRestOperations(restTemplate()) - - return accessTokenResponseClient - } - - @Bean - fun jwtBearerAccessTokenResponseClient(): OAuth2AccessTokenResponseClient { - val accessTokenResponseClient = DefaultJwtBearerTokenResponseClient() - accessTokenResponseClient.setRestOperations(restTemplate()) - - return accessTokenResponseClient - } - - @Bean - fun restTemplate(): RestTemplate { - // ... - } - -} ----- -===== - -A default `OAuth2AuthorizedClientManager` will be published automatically by Spring Security when one is not already provided. - -[TIP] -==== -Notice that we don't need to customize the `SecurityFilterChain` bean in this case, and can stick with the defaults. -If using Spring Boot with no additional customizations, we can actually omit the `SecurityFilterChain` bean entirely. -==== - -Prior to Spring Security 6.2, we had to ensure this customization was applied to both OAuth2 Login (if we are using this feature) and OAuth2 Client components. -We had to use both the Spring Security DSL (for the `authorization_code` grant) and publish a bean of type `OAuth2AuthorizedClientManager` for other grant types. -To understand what is being configured behind the scenes, here's what the configuration might have looked like: - -.Customize `RestOperations` for OAuth2 Client (prior to 6.2) -[tabs] -===== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = - new DefaultAuthorizationCodeTokenResponseClient(); - accessTokenResponseClient.setRestOperations(restTemplate()); - - http - // ... - .oauth2Login((oauth2Login) -> oauth2Login - .tokenEndpoint((tokenEndpoint) -> tokenEndpoint - .accessTokenResponseClient(accessTokenResponseClient) - ) - ) - .oauth2Client((oauth2Client) -> oauth2Client - .authorizationCodeGrant((authorizationCode) -> authorizationCode - .accessTokenResponseClient(accessTokenResponseClient) - ) - ); - - return http.build(); - } - - @Bean - public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository) { - - DefaultRefreshTokenTokenResponseClient refreshTokenAccessTokenResponseClient = - new DefaultRefreshTokenTokenResponseClient(); - refreshTokenAccessTokenResponseClient.setRestOperations(restTemplate()); - - DefaultClientCredentialsTokenResponseClient clientCredentialsAccessTokenResponseClient = - new DefaultClientCredentialsTokenResponseClient(); - clientCredentialsAccessTokenResponseClient.setRestOperations(restTemplate()); - - DefaultPasswordTokenResponseClient passwordAccessTokenResponseClient = - new DefaultPasswordTokenResponseClient(); - passwordAccessTokenResponseClient.setRestOperations(restTemplate()); - - DefaultJwtBearerTokenResponseClient jwtBearerAccessTokenResponseClient = - new DefaultJwtBearerTokenResponseClient(); - jwtBearerAccessTokenResponseClient.setRestOperations(restTemplate()); - - JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = - new JwtBearerOAuth2AuthorizedClientProvider(); - jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient); - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken((refreshToken) -> refreshToken - .accessTokenResponseClient(refreshTokenAccessTokenResponseClient) - ) - .clientCredentials((clientCredentials) -> clientCredentials - .accessTokenResponseClient(clientCredentialsAccessTokenResponseClient) - ) - .password((password) -> password - .accessTokenResponseClient(passwordAccessTokenResponseClient) - ) - .provider(jwtBearerAuthorizedClientProvider) - .build(); - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; - } - - @Bean - public RestTemplate restTemplate() { - // ... - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -import org.springframework.security.config.annotation.web.invoke - -@Configuration -@EnableWebSecurity -class SecurityConfig { - - @Bean - fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { - val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient() - tokenResponseClient.setRestOperations(restTemplate()) - - http { - // ... - oauth2Login { - tokenEndpoint { - accessTokenResponseClient = tokenResponseClient - } - } - oauth2Client { - authorizationCodeGrant { - accessTokenResponseClient = tokenResponseClient - } - } - } - - return http.build() - } - - @Bean - fun authorizedClientManager( - clientRegistrationRepository: ClientRegistrationRepository?, - authorizedClientRepository: OAuth2AuthorizedClientRepository? - ): OAuth2AuthorizedClientManager { - val refreshTokenAccessTokenResponseClient = DefaultRefreshTokenTokenResponseClient() - refreshTokenAccessTokenResponseClient.setRestOperations(restTemplate()) - - val clientCredentialsAccessTokenResponseClient = DefaultClientCredentialsTokenResponseClient() - clientCredentialsAccessTokenResponseClient.setRestOperations(restTemplate()) - - val passwordAccessTokenResponseClient = DefaultPasswordTokenResponseClient() - passwordAccessTokenResponseClient.setRestOperations(restTemplate()) - - val jwtBearerAccessTokenResponseClient = DefaultJwtBearerTokenResponseClient() - jwtBearerAccessTokenResponseClient.setRestOperations(restTemplate()) - - val jwtBearerAuthorizedClientProvider = JwtBearerOAuth2AuthorizedClientProvider() - jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient) - - val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken { refreshToken -> - refreshToken.accessTokenResponseClient(refreshTokenAccessTokenResponseClient) - } - .clientCredentials { clientCredentials -> - clientCredentials.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient) - } - .password { password -> - password.accessTokenResponseClient(passwordAccessTokenResponseClient) - } - .provider(jwtBearerAuthorizedClientProvider) - .build() - - val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository - ) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - - return authorizedClientManager - } - - @Bean - fun restTemplate(): RestTemplate { - // ... - } - -} ----- -===== - - -[[further-reading]] -== Further Reading - -This preceding sections introduced Spring Security's support for OAuth2 with examples for common scenarios. -You can read more about OAuth2 Client and Resource Server in the following sections of the reference documentation: - -* xref:servlet/oauth2/login/index.adoc[] -* xref:servlet/oauth2/client/index.adoc[] -* xref:servlet/oauth2/resource-server/index.adoc[] diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc index 9d5fa9918a..a0ee7eeced 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc @@ -929,5 +929,111 @@ For MAC-based algorithms (such as `HS256`, `HS384`, or `HS512`), the `client-sec If more than one `ClientRegistration` is configured for OpenID Connect 1.0 Authentication, the JWS algorithm resolver may evaluate the provided `ClientRegistration` to determine which algorithm to return. ==== + [[oauth2login-advanced-oidc-logout]] -Then, you can proceed to configure xref:reactive/oauth2/login/logout.adoc[logout] +== OpenID Connect 1.0 Logout + +OpenID Connect Session Management 1.0 allows the ability to log out the end user at the Provider by using the Client. +One of the strategies available is https://openid.net/specs/openid-connect-rpinitiated-1_0.html[RP-Initiated Logout]. + +If the OpenID Provider supports both Session Management and https://openid.net/specs/openid-connect-discovery-1_0.html[Discovery], the client can obtain the `end_session_endpoint` `URL` from the OpenID Provider's https://openid.net/specs/openid-connect-session-1_0.html#OPMetadata[Discovery Metadata]. +You can do so by configuring the `ClientRegistration` with the `issuer-uri`, as follows: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + ... + provider: + okta: + issuer-uri: https://dev-1234.oktapreview.com +---- + +Also, you can configure `OidcClientInitiatedLogoutSuccessHandler`, which implements RP-Initiated Logout, as follows: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +@EnableWebSecurity +public class OAuth2LoginSecurityConfig { + + @Autowired + private ClientRegistrationRepository clientRegistrationRepository; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2Login(withDefaults()) + .logout(logout -> logout + .logoutSuccessHandler(oidcLogoutSuccessHandler()) + ); + return http.build(); + } + + private LogoutSuccessHandler oidcLogoutSuccessHandler() { + OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler = + new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository); + + // Sets the location that the End-User's User Agent will be redirected to + // after the logout has been performed at the Provider + oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}"); + + return oidcLogoutSuccessHandler; + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +@EnableWebSecurity +class OAuth2LoginSecurityConfig { + @Autowired + private lateinit var clientRegistrationRepository: ClientRegistrationRepository + + @Bean + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2Login { } + logout { + logoutSuccessHandler = oidcLogoutSuccessHandler() + } + } + return http.build() + } + + private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler { + val oidcLogoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository) + + // Sets the location that the End-User's User Agent will be redirected to + // after the logout has been performed at the Provider + oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}") + return oidcLogoutSuccessHandler + } +} +---- +====== + +[NOTE] +==== +`OidcClientInitiatedLogoutSuccessHandler` supports the `+{baseUrl}+` placeholder. +If used, the application's base URL, such as `https://app.example.org`, replaces it at request time. +==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc deleted file mode 100644 index 24078cf61f..0000000000 --- a/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc +++ /dev/null @@ -1,267 +0,0 @@ -= OIDC Logout - -Once an end user is able to login to your application, it's important to consider how they will log out. - -Generally speaking, there are three use cases for you to consider: - -1. I want to perform only a local logout -2. I want to log out both my application and the OIDC Provider, initiated by my application -3. I want to log out both my application and the OIDC Provider, initiated by the OIDC Provider - -[[configure-local-logout]] -== Local Logout - -To perform a local logout, no special OIDC configuration is needed. -Spring Security automatically stands up a local logout endpoint, which you can xref:servlet/authentication/logout.adoc[configure through the `logout()` DSL]. - -[[configure-client-initiated-oidc-logout]] -== OpenID Connect 1.0 Client-Initiated Logout - -OpenID Connect Session Management 1.0 allows the ability to log out the end user at the Provider by using the Client. -One of the strategies available is https://openid.net/specs/openid-connect-rpinitiated-1_0.html[RP-Initiated Logout]. - -If the OpenID Provider supports both Session Management and https://openid.net/specs/openid-connect-discovery-1_0.html[Discovery], the client can obtain the `end_session_endpoint` `URL` from the OpenID Provider's https://openid.net/specs/openid-connect-session-1_0.html#OPMetadata[Discovery Metadata]. -You can do so by configuring the `ClientRegistration` with the `issuer-uri`, as follows: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-secret: okta-client-secret - ... - provider: - okta: - issuer-uri: https://dev-1234.oktapreview.com ----- - -Also, you should configure `OidcClientInitiatedLogoutSuccessHandler`, which implements RP-Initiated Logout, as follows: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class OAuth2LoginSecurityConfig { - - @Autowired - private ClientRegistrationRepository clientRegistrationRepository; - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2Login(withDefaults()) - .logout(logout -> logout - .logoutSuccessHandler(oidcLogoutSuccessHandler()) - ); - return http.build(); - } - - private LogoutSuccessHandler oidcLogoutSuccessHandler() { - OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler = - new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository); - - // Sets the location that the End-User's User Agent will be redirected to - // after the logout has been performed at the Provider - oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}"); - - return oidcLogoutSuccessHandler; - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -@EnableWebSecurity -class OAuth2LoginSecurityConfig { - @Autowired - private lateinit var clientRegistrationRepository: ClientRegistrationRepository - - @Bean - open fun filterChain(http: HttpSecurity): SecurityFilterChain { - http { - authorizeHttpRequests { - authorize(anyRequest, authenticated) - } - oauth2Login { } - logout { - logoutSuccessHandler = oidcLogoutSuccessHandler() - } - } - return http.build() - } - - private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler { - val oidcLogoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository) - - // Sets the location that the End-User's User Agent will be redirected to - // after the logout has been performed at the Provider - oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}") - return oidcLogoutSuccessHandler - } -} ----- -====== - -[NOTE] -==== -`OidcClientInitiatedLogoutSuccessHandler` supports the `+{baseUrl}+` placeholder. -If used, the application's base URL, such as `https://app.example.org`, replaces it at request time. -==== - -[[configure-provider-initiated-oidc-logout]] -== OpenID Connect 1.0 Back-Channel Logout - -OpenID Connect Session Management 1.0 allows the ability to log out the end user at the Client by having the Provider make an API call to the Client. -This is referred to as https://openid.net/specs/openid-connect-backchannel-1_0.html[OIDC Back-Channel Logout]. - -To enable this, you can stand up the Back-Channel Logout endpoint in the DSL like so: - -[tabs] -====== -Java:: -+ -[source=java,role="primary"] ----- -@Bean -public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests((authorize) -> authorize - .anyRequest().authenticated() - ) - .oauth2Login(withDefaults()) - .oidcLogout((logout) -> logout - .backChannel(Customizer.withDefaults()) - ); - return http.build(); -} ----- - -Kotlin:: -+ -[source=kotlin,role="secondary"] ----- -@Bean -open fun filterChain(http: HttpSecurity): SecurityFilterChain { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2Login { } - oidcLogout { - backChannel { } - } - } - return http.build() -} ----- -====== - -And that's it! - -This will stand up the endpoint `+/logout/connect/back-channel/{registrationId}+` which the OIDC Provider can request to invalidate a given session of an end user in your application. - -[NOTE] -`oidcLogout` requires that `oauth2Login` also be configured. - -[NOTE] -`oidcLogout` requires that the session cookie be called `JSESSIONID` in order to correctly log out each session through a backchannel. - -=== Back-Channel Logout Architecture - -Consider a `ClientRegistration` whose identifier is `registrationId`. - -The overall flow for a Back-Channel logout is like this: - -1. At login time, Spring Security correlates the ID Token, CSRF Token, and Provider Session ID (if any) to your application's session id in its `OidcSessionStrategy` implementation. -2. Then at logout time, your OIDC Provider makes an API call to `/logout/connect/back-channel/registrationId` including a Logout Token that indicates either the `sub` (the End User) or the `sid` (the Provider Session ID) to logout. -3. Spring Security validates the token's signature and claims. -4. If the token contains a `sid` claim, then only the Client's session that correlates to that provider session is terminated. -5. Otherwise, if the token contains a `sub` claim, then all that Client's sessions for that End User are terminated. - -[NOTE] -Remember that Spring Security's OIDC support is multi-tenant. -This means that it will only terminate sessions whose Client matches the `aud` claim in the Logout Token. - -=== Customizing the OIDC Provider Session Strategy - -By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session. - -There are a number of circumstances, like a clustered application, where it would be nice to store this instead in a separate location, like a database. - -You can achieve this by configuring a custom `OidcSessionStrategy`, like so: - -[tabs] -====== -Java:: -+ -[source=java,role="primary"] ----- -@Component -public final class MySpringDataOidcSessionStrategy implements OidcSessionStrategy { - private final OidcProviderSessionRepository sessions; - - // ... - - @Override - public void saveSessionInformation(OidcSessionInformation info) { - this.sessions.save(info); - } - - @Override - public OidcSessionInformation(String clientSessionId) { - return this.sessions.removeByClientSessionId(clientSessionId); - } - - @Override - public Iterable removeSessionInformation(OidcLogoutToken token) { - return token.getSessionId() != null ? - this.sessions.removeBySessionIdAndIssuerAndAudience(...) : - this.sessions.removeBySubjectAndIssuerAndAudience(...); - } -} ----- - -Kotlin:: -+ -[source=kotlin,role="secondary"] ----- -@Component -class MySpringDataOidcSessionStrategy: OidcSessionStrategy { - val sessions: OidcProviderSessionRepository - - // ... - - @Override - fun saveSessionInformation(info: OidcSessionInformation) { - this.sessions.save(info) - } - - @Override - fun removeSessionInformation(clientSessionId: String): OidcSessionInformation { - return this.sessions.removeByClientSessionId(clientSessionId); - } - - @Override - fun removeSessionInformation(token: OidcLogoutToken): Iterable { - return token.getSessionId() != null ? - this.sessions.removeBySessionIdAndIssuerAndAudience(...) : - this.sessions.removeBySubjectAndIssuerAndAudience(...); - } -} ----- -====== - diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc index 3a3eae8ea7..52e991a0f4 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc @@ -27,7 +27,7 @@ The figure above builds off our xref:servlet/architecture.adoc#servlet-securityf image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the `/private` resource for which the user is not authorized. -image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] indicates that the unauthenticated request is _Denied_ by throwing an `AccessDeniedException`. +image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is _Denied_ by throwing an `AccessDeniedException`. image:{icondir}/number_3.png[] Since the user is not authenticated, xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates _Start Authentication_. The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationEntryPoint.html[`BearerTokenAuthenticationEntryPoint`], which sends a `WWW-Authenticate` header. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc index 5353163642..9ea554c564 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc @@ -176,7 +176,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) - .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults())); + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); return http.build(); } ---- @@ -211,8 +211,6 @@ Java:: + [source,java,role="primary"] ---- -import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope; - @Configuration @EnableWebSecurity public class MyCustomSecurityConfiguration { @@ -220,7 +218,7 @@ public class MyCustomSecurityConfiguration { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/messages/**").access(hasScope("message:read")) + .requestMatchers("/messages/**").hasAuthority("SCOPE_message:read") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -237,8 +235,6 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope - @Configuration @EnableWebSecurity class MyCustomSecurityConfiguration { @@ -246,7 +242,7 @@ class MyCustomSecurityConfiguration { open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { - authorize("/messages/**", hasScope("message:read")) + authorize("/messages/**", hasAuthority("SCOPE_message:read")) authorize(anyRequest, authenticated) } oauth2ResourceServer { @@ -479,8 +475,7 @@ This is handy when deeper configuration, like <> `@Bean` has the same effect as `decoder()`. -You can construct one with a `jwkSetUri` like so: +Or, exposing a <> `@Bean` has the same effect as `decoder()`: [tabs] ====== @@ -505,56 +500,6 @@ fun jwtDecoder(): JwtDecoder { ---- ====== -or you can use the issuer and have `NimbusJwtDecoder` look up the `jwkSetUri` when `build()` is invoked, like the following: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -public JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withIssuerLocation(issuer).build(); -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withIssuerLocation(issuer).build() -} ----- -====== - -Or, if the defaults work for you, you can also use `JwtDecoders`, which does the above in addition to configuring the decoder's validator: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -public JwtDecoders jwtDecoder() { - return JwtDecoders.fromIssuerLocation(issuer); -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Bean -fun jwtDecoder(): JwtDecoders { - return JwtDecoders.fromIssuerLocation(issuer) -} ----- -====== - [[oauth2resourceserver-jwt-decoder-algorithm]] == Configuring Trusted Algorithms @@ -591,7 +536,7 @@ Java:: ---- @Bean JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withIssuerLocation(this.issuer) + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithm(RS512).build(); } ---- @@ -602,7 +547,7 @@ Kotlin:: ---- @Bean fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withIssuerLocation(this.issuer) + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithm(RS512).build() } ---- @@ -618,7 +563,7 @@ Java:: ---- @Bean JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withIssuerLocation(this.issuer) + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build(); } ---- @@ -629,7 +574,7 @@ Kotlin:: ---- @Bean fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withIssuerLocation(this.issuer) + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build() } ---- @@ -645,7 +590,7 @@ Java:: ---- @Bean JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withIssuerLocation(this.issuer) + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithms(algorithms -> { algorithms.add(RS512); algorithms.add(ES512); @@ -659,7 +604,7 @@ Kotlin:: ---- @Bean fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withIssuerLocation(this.issuer) + return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithms { it.add(RS512) it.add(ES512) @@ -866,8 +811,6 @@ Java:: + [source,java,role="primary"] ---- -import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope; - @Configuration @EnableWebSecurity public class DirectlyConfiguredJwkSetUri { @@ -875,8 +818,8 @@ public class DirectlyConfiguredJwkSetUri { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/contacts/**").access(hasScope("contacts")) - .requestMatchers("/messages/**").access(hasScope("messages")) + .requestMatchers("/contacts/**").hasAuthority("SCOPE_contacts") + .requestMatchers("/messages/**").hasAuthority("SCOPE_messages") .anyRequest().authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); @@ -889,8 +832,6 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope; - @Configuration @EnableWebSecurity class DirectlyConfiguredJwkSetUri { @@ -898,8 +839,8 @@ class DirectlyConfiguredJwkSetUri { open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { - authorize("/contacts/**", hasScope("contacts")) - authorize("/messages/**", hasScope("messages")) + authorize("/contacts/**", hasAuthority("SCOPE_contacts")) + authorize("/messages/**", hasAuthority("SCOPE_messages")) authorize(anyRequest, authenticated) } oauth2ResourceServer { @@ -1373,7 +1314,7 @@ Java:: ---- @Bean JwtDecoder jwtDecoder() { - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build(); + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub)); @@ -1389,7 +1330,7 @@ Kotlin:: ---- @Bean fun jwtDecoder(): JwtDecoder { - val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build() + val jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build() val converter = MappedJwtClaimSetConverter .withDefaults(mapOf("sub" to this::lookupUserIdBySub)) @@ -1497,7 +1438,7 @@ Java:: ---- @Bean JwtDecoder jwtDecoder() { - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build(); + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter()); return jwtDecoder; } @@ -1509,7 +1450,7 @@ Kotlin:: ---- @Bean fun jwtDecoder(): JwtDecoder { - val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build() + val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build() jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter()) return jwtDecoder } @@ -1539,7 +1480,7 @@ public JwtDecoder jwtDecoder(RestTemplateBuilder builder) { .setReadTimeout(Duration.ofSeconds(60)) .build(); - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build(); + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build(); return jwtDecoder; } ---- @@ -1554,7 +1495,7 @@ fun jwtDecoder(builder: RestTemplateBuilder): JwtDecoder { .setConnectTimeout(Duration.ofSeconds(60)) .setReadTimeout(Duration.ofSeconds(60)) .build() - return NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build() + return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build() } ---- ====== @@ -1572,7 +1513,7 @@ Java:: ---- @Bean public JwtDecoder jwtDecoder(CacheManager cacheManager) { - return NimbusJwtDecoder.withIssuerLocation(issuer) + return NimbusJwtDecoder.withJwkSetUri(jwkSetUri) .cache(cacheManager.getCache("jwks")) .build(); } @@ -1584,7 +1525,7 @@ Kotlin:: ---- @Bean fun jwtDecoder(cacheManager: CacheManager): JwtDecoder { - return NimbusJwtDecoder.withIssuerLocation(issuer) + return NimbusJwtDecoder.withJwkSetUri(jwkSetUri) .cache(cacheManager.getCache("jwks")) .build() } diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc index c13abb7704..0cdc14aee8 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc @@ -114,8 +114,8 @@ Java:: + [source,java,role="primary"] ---- -JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver - .fromTrustedIssuers("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo"); +JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver + ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo"); http .authorizeHttpRequests(authorize -> authorize @@ -131,7 +131,7 @@ Kotlin:: [source,kotlin,role="secondary"] ---- val customAuthenticationManagerResolver = JwtIssuerAuthenticationManagerResolver - .fromTrustedIssuers("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo") + ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo") http { authorizeRequests { authorize(anyRequest, authenticated) diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc index fcd3b4f2c4..45a6f20be9 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc @@ -239,8 +239,6 @@ Java:: + [source,java,role="primary"] ---- -import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope; - @Configuration @EnableWebSecurity public class MyCustomSecurityConfiguration { @@ -248,7 +246,7 @@ public class MyCustomSecurityConfiguration { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/messages/**").access(hasScope("message:read")) + .requestMatchers("/messages/**").hasAuthority("SCOPE_message:read") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -265,8 +263,6 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope; - @Configuration @EnableWebSecurity class MyCustomSecurityConfiguration { @@ -274,7 +270,7 @@ class MyCustomSecurityConfiguration { open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { - authorize("/messages/**", hasScope("SCOPE_message:read")) + authorize("/messages/**", hasAuthority("SCOPE_message:read")) authorize(anyRequest, authenticated) } oauth2ResourceServer { @@ -551,8 +547,6 @@ Java:: + [source,java,role="primary"] ---- -import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope; - @Configuration @EnableWebSecurity public class MappedAuthorities { @@ -560,8 +554,8 @@ public class MappedAuthorities { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorizeRequests -> authorizeRequests - .requestMatchers("/contacts/**").access(hasScope("contacts")) - .requestMatchers("/messages/**").access(hasScope("messages")) + .requestMatchers("/contacts/**").hasAuthority("SCOPE_contacts") + .requestMatchers("/messages/**").hasAuthority("SCOPE_messages") .anyRequest().authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); @@ -574,8 +568,6 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope - @Configuration @EnableWebSecurity class MappedAuthorities { @@ -583,8 +575,8 @@ class MappedAuthorities { open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeRequests { - authorize("/contacts/**", hasScope("contacts")) - authorize("/messages/**", hasScope("messages")) + authorize("/contacts/**", hasAuthority("SCOPE_contacts")) + authorize("/messages/**", hasAuthority("SCOPE_messages")) authorize(anyRequest, authenticated) } oauth2ResourceServer { diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc index 7cae584007..3880d1902a 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc @@ -13,11 +13,38 @@ You can configure this in a number of ways including: To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL. -[[saml2-response-processing-endpoint]] -== Changing the SAML Response Processing Endpoint +[[relyingpartyregistrationresolver-apply]] +== Changing `RelyingPartyRegistration` Lookup -The default endpoint is `+/login/saml2/sso/{registrationId}+`. -You can change this in the DSL and in the associated metadata like so: +`RelyingPartyRegistration` lookup is customized xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-relyingpartyregistrationresolver[in a `RelyingPartyRegistrationResolver`]. + +To apply a `RelyingPartyRegistrationResolver` when processing `` payloads, you should first publish a `Saml2AuthenticationTokenConverter` bean like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +Saml2AuthenticationTokenConverter authenticationConverter(InMemoryRelyingPartyRegistrationRepository registrations) { + return new Saml2AuthenticationTokenConverter(new MyRelyingPartyRegistrationResolver(registrations)); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun authenticationConverter(val registrations: InMemoryRelyingPartyRegistrationRepository): Saml2AuthenticationTokenConverter { + return Saml2AuthenticationTokenConverter(MyRelyingPartyRegistrationResolver(registrations)); +} +---- +====== + +Recall that the Assertion Consumer Service URL is `+/saml2/login/sso/{registrationId}+` by default. +If you are no longer wanting the `registrationId` in the URL, change it in the filter chain and in your relying party metadata: [tabs] ====== @@ -63,59 +90,14 @@ Java:: + [source,java,role="primary"] ---- -relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO") +relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso") ---- Kotlin:: + [source,kotlin,role="secondary"] ---- -relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO") ----- -====== - -[[relyingpartyregistrationresolver-apply]] -== Changing `RelyingPartyRegistration` lookup - -By default, this converter will match against any associated `` or any `registrationId` it finds in the URL. -Or, if it cannot find one in either of those cases, then it attempts to look it up by the `` element. - -There are a number of circumstances where you might need something more sophisticated, like if you are supporting `ARTIFACT` binding. -In those cases, you can customize lookup through a custom `AuthenticationConverter`, which you can customize like so: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -SecurityFilterChain securityFilters(HttpSecurity http, AuthenticationConverter authenticationConverter) throws Exception { - http - // ... - .saml2Login((saml2) -> saml2.authenticationConverter(authenticationConverter)) - // ... - - return http.build(); -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Bean -fun securityFilters(val http: HttpSecurity, val converter: AuthenticationConverter): SecurityFilterChain { - http { - // ... - .saml2Login { - authenticationConverter = converter - } - // ... - } - - return http.build() -} +relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso") ---- ====== diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc index 4600e9a8b8..eda16b4093 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc @@ -16,7 +16,7 @@ The figure above builds off our xref:servlet/architecture.adoc#servlet-securityf image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the `/private` resource, for which it is not authorized. -image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] indicates that the unauthenticated request is _Denied_ by throwing an `AccessDeniedException`. +image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is _Denied_ by throwing an `AccessDeniedException`. image:{icondir}/number_3.png[] Since the user lacks authorization, the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates _Start Authentication_. The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[`LoginUrlAuthenticationEntryPoint`], which redirects to <` generating endpoint>>, `Saml2WebSsoAuthenticationRequestFilter`. @@ -690,9 +690,9 @@ In a deployed application, that translates to: The prevailing URI patterns are as follows: * `+/saml2/authenticate/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication-requests.adoc[generates a ``] based on the configurations for that `RelyingPartyRegistration` and sends it to the asserting party -* `+/login/saml2/sso/+` - The endpoint that xref:servlet/saml2/login/authentication.adoc[authenticates an asserting party's ``]; the `RelyingPartyRegistration` is looked up from previously authenticated state or the response's issuer if needed; also supports `+/login/saml2/sso/{registrationId}+` -* `+/logout/saml2/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `` and `` payloads]; the `RelyingPartyRegistration` is looked up from previously authenticated state or the request's issuer if needed; also supports `+/logout/saml2/slo/{registrationId}+` -* `+/saml2/metadata+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for the set of ``RelyingPartyRegistration``s; also supports `+/saml2/metadata/{registrationId}+` or `+/saml2/service-provider-metadata/{registrationId}+` for a specific `RelyingPartyRegistration` +* `+/saml2/login/sso/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication.adoc[authenticates an asserting party's ``] based on the configurations for that `RelyingPartyRegistration` +* `+/saml2/logout/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `` and `` payloads]; the `RelyingPartyRegistration` is looked up from previously authenticated state +* `+/saml2/saml2-service-provider/metadata/{registrationId}+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for that `RelyingPartyRegistration` Since the `registrationId` is the primary identifier for a `RelyingPartyRegistration`, it is needed in the URL for unauthenticated scenarios. If you wish to remove the `registrationId` from the URL for any reason, you can <> to tell Spring Security how to look up the `registrationId`. @@ -880,18 +880,128 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? { As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path. -Depending on the use case, a number of other strategies are also employed to derive one. -For example: +There are a number of reasons you may want to customize that. Among them: -* For processing ``s, the `RelyingPartyRegistration` is looked up from the associated `` or from the `` element -* For processing ``s, the `RelyingPartyRegistration` is looked up from the currently logged in user or from the `` element -* For publishing metadata, the `RelyingPartyRegistration`s are looked up from any repository that also implements `Iterable` +* You may already <> +* You may be <> -When this needs adjustment, you can turn to the specific components for each of these endpoints targeted at customizing this: +To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`. +The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`. -* For SAML Responses, customize the `AuthenticationConverter` -* For Logout Requests, customize the `Saml2LogoutRequestValidatorParametersResolver` -* For Metadata, customize the `Saml2MetadataResponseResolver` +[NOTE] +Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them. + +[[relyingpartyregistrationresolver-single]] +==== Resolving to a Single Consistent `RelyingPartyRegistration` + +You can provide a resolver that, for example, always returns the same `RelyingPartyRegistration`: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver { + + private final RelyingPartyRegistrationResolver delegate; + + public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) { + this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations); + } + + @Override + public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) { + return this.delegate.resolve(request, "single"); + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver { + override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? { + return this.delegate.resolve(request, "single") + } +} +---- +====== + +[TIP] +You might next take a look at how to use this resolver to customize xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[`` metadata production]. + +[[relyingpartyregistrationresolver-entityid]] +==== Resolving Based on the `` + +When you have one relying party that can accept assertions from multiple asserting parties, you will have as many ``RelyingPartyRegistration``s as asserting parties, with the <>. + +This carries the implication that the assertion consumer service endpoint will be different for each asserting party, which may not be desirable. + +You can instead resolve the `registrationId` via the `Issuer`. +A custom implementation of `RelyingPartyRegistrationResolver` that does this may look like: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +public class SamlResponseIssuerRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver { + private final InMemoryRelyingPartyRegistrationRepository registrations; + + // ... constructor + + @Override + RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) { + if (registrationId != null) { + return this.registrations.findByRegistrationId(registrationId); + } + String entityId = resolveEntityIdFromSamlResponse(request); + for (RelyingPartyRegistration registration : this.registrations) { + if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) { + return registration; + } + } + return null; + } + + private String resolveEntityIdFromSamlResponse(HttpServletRequest request) { + // ... + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +class SamlResponseIssuerRelyingPartyRegistrationResolver(val registrations: InMemoryRelyingPartyRegistrationRepository): + RelyingPartyRegistrationResolver { + @Override + fun resolve(val request: HttpServletRequest, val registrationId: String): RelyingPartyRegistration { + if (registrationId != null) { + return this.registrations.findByRegistrationId(registrationId) + } + String entityId = resolveEntityIdFromSamlResponse(request) + for (val registration : this.registrations) { + if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) { + return registration + } + } + return null + } + + private resolveEntityIdFromSamlResponse(val request: HttpServletRequest): String { + // ... + } +} +---- +====== + +[TIP] +You might next take a look at how to use this resolver to customize xref:servlet/saml2/login/authentication.adoc#relyingpartyregistrationresolver-apply[`` authentication]. [[federating-saml2-login]] === Federating Login @@ -926,7 +1036,6 @@ var registrations: Collection = RelyingPartyRegistrati .stream().map { builder : RelyingPartyRegistration.Builder -> builder .registrationId(UUID.randomUUID().toString()) .entityId("https://example.org/saml2/sp") - .assertionConsumerServiceLocation("{baseUrl}/login/saml2/sso") .build() } .collect(Collectors.toList())); @@ -937,7 +1046,15 @@ Note that because the registration id is set to a random value, this will change There are several ways to address this; let's focus on a way that suits the specific use case of federation. In many federation cases, all the asserting parties share service provider configuration. -Given that Spring Security will by default include the `registrationId` in the service provider metadata, another step is to change corresponding URIs to exclude the `registrationId`, which you can see has already been done in the above sample where the `entityId` and `assertionConsumerServiceLocation` are configured with a static endpoint. +Given that Spring Security will by default include the `registrationId` in all many of its SAML 2.0 URIs, the next step is often to change these URIs to exclude the `registrationId`. + +There are two main URIs you will want to change along those lines: + +* <`>> +* <> + +[NOTE] +Optionally, you may also want to change the Authentication Request location, but since this is a URI internal to the app and not published to asserting parties, the benefit is often minimal. You can see a completed example of this in {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample]. diff --git a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc index d737bf578a..87eb5528b3 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc @@ -132,9 +132,9 @@ To add other values, you can use delegation, like so: [source,java] ---- @Bean -Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) { - OpenSaml4LogoutRequestResolver logoutRequestResolver = - new OpenSaml4LogoutRequestResolver(registrations); +Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationResolver registrationResolver) { + OpenSaml4LogoutRequestResolver logoutRequestResolver + new OpenSaml4LogoutRequestResolver(registrationResolver); logoutRequestResolver.setParametersConsumer((parameters) -> { String name = ((Saml2AuthenticatedPrincipal) parameters.getAuthentication().getPrincipal()).getFirstAttribute("CustomAttribute"); String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"; @@ -175,9 +175,9 @@ To add other values, you can use delegation, like so: [source,java] ---- @Bean -public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) { +public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationResolver registrationResolver) { OpenSaml4LogoutResponseResolver logoutRequestResolver = - new OpenSaml4LogoutResponseResolver(registrations); + new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver); logoutRequestResolver.setParametersConsumer((parameters) -> { if (checkOtherPrevailingConditions(parameters.getRequest())) { parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT); diff --git a/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc b/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc index 4dd00440a1..20e74a626f 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc @@ -35,7 +35,7 @@ val openSamlEntityDescriptor: EntityDescriptor = details.getEntityDescriptor(); [[publishing-relying-party-metadata]] == Producing `` Metadata -You can publish a metadata endpoint using the `saml2Metadata` DSL method, as you'll see below: +You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below: [tabs] ====== @@ -43,20 +43,33 @@ Java:: + [source,java,role="primary"] ---- +DefaultRelyingPartyRegistrationResolver relyingPartyRegistrationResolver = + new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository); +Saml2MetadataFilter filter = new Saml2MetadataFilter( + relyingPartyRegistrationResolver, + new OpenSamlMetadataResolver()); + http // ... .saml2Login(withDefaults()) - .saml2Metadata(withDefaults()); + .addFilterBefore(filter, Saml2WebSsoAuthenticationFilter.class); ---- Kotlin:: + [source,kotlin,role="secondary"] ---- +val relyingPartyRegistrationResolver: Converter = + DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository) +val filter = Saml2MetadataFilter( + relyingPartyRegistrationResolver, + OpenSamlMetadataResolver() +) + http { //... saml2Login { } - saml2Metadata { } + addFilterBefore(filter) } ---- ====== @@ -64,9 +77,8 @@ http { You can use this metadata endpoint to register your relying party with your asserting party. This is often as simple as finding the correct form field to supply the metadata endpoint. -By default, the metadata endpoint is `+/saml2/metadata+`, though it also responds to `+/saml2/metadata/{registrationId}+` and `+/saml2/service-provider-metadata/{registrationId}+`. - -You can change this by calling the `metadataUrl` method in the DSL: +By default, the metadata endpoint is `+/saml2/service-provider-metadata/{registrationId}+`. +You can change this by calling the `setRequestMatcher` method on the filter: [tabs] ====== @@ -74,22 +86,39 @@ Java:: + [source,java,role="primary"] ---- -.saml2Metadata((saml2) -> saml2.metadataUrl("/saml/metadata")) +filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET")); ---- Kotlin:: + [source,kotlin,role="secondary"] ---- -saml2Metadata { - metadataUrl = "/saml/metadata" -} +filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET")) +---- +====== + +Or, if you have registered a custom relying party registration resolver in the constructor, then you can specify a path without a `registrationId` hint, like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET")); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET")) ---- ====== == Changing the Way a `RelyingPartyRegistration` Is Looked Up -If you have a different strategy for identifying which `RelyingPartyRegistration` to use, you can configure your own `Saml2MetadataResponseResolver` like the one below: +To apply a custom `RelyingPartyRegistrationResolver` to the metadata endpoint, you can provide it directly in the filter constructor like so: [tabs] ====== @@ -97,25 +126,38 @@ Java:: + [source,java,role="primary"] ---- -@Bean -Saml2MetadataResponseResolver metadataResponseResolver(RelyingPartyRegistrationRepository registrations) { - RequestMatcherMetadataResponseResolver metadata = new RequestMatcherMetadataResponseResolver( - (id) -> registrations.findByRegistrationId("relying-party")); - metadata.setMetadataFilename("metadata.xml"); - return metadata; -} ----- +RelyingPartyRegistrationResolver myRegistrationResolver = ...; +Saml2MetadataFilter metadata = new Saml2MetadataFilter(myRegistrationResolver, new OpenSamlMetadataResolver()); -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Bean -fun metadataResponseResolver(val registrations: RelyingPartyRegistrationRepository): Saml2MetadataResponseResolver { - val metadata = new RequestMatcherMetadataResponseResolver( - id: String -> registrations.findByRegistrationId("relying-party")) - metadata.setMetadataFilename("metadata.xml") - return metadata -} +// ... + +http.addFilterBefore(metadata, BasicAuthenticationFilter.class); ---- ====== + +.Kotlin +---- +val myRegistrationResolver: RelyingPartyRegistrationResolver = ...; +val metadata = new Saml2MetadataFilter(myRegistrationResolver, OpenSamlMetadataResolver()); + +// ... + +http.addFilterBefore(metadata, BasicAuthenticationFilter::class.java); +---- + +In the event that you are applying a `RelyingPartyRegistrationResolver` to remove the `registrationId` from the URI, you must also change the URI in the filter like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +metadata.setRequestMatcher("/saml2/metadata") +---- +====== + +.Kotlin +---- +metadata.setRequestMatcher("/saml2/metadata") +---- diff --git a/docs/modules/ROOT/pages/servlet/test/mockmvc/request-builders.adoc b/docs/modules/ROOT/pages/servlet/test/mockmvc/request-builders.adoc index f62ddd79c1..ebbf07c68d 100644 --- a/docs/modules/ROOT/pages/servlet/test/mockmvc/request-builders.adoc +++ b/docs/modules/ROOT/pages/servlet/test/mockmvc/request-builders.adoc @@ -1,4 +1,4 @@ -= SecurityMockMvcRequestBuilders +== SecurityMockMvcRequestBuilders Spring MVC Test also provides a `RequestBuilder` interface that can be used to create the `MockHttpServletRequest` used in your test. Spring Security provides a few `RequestBuilder` implementations that can be used to make testing easier. diff --git a/docs/modules/ROOT/pages/servlet/test/mockmvc/result-handlers.adoc b/docs/modules/ROOT/pages/servlet/test/mockmvc/result-handlers.adoc index 778469106d..48c767f468 100644 --- a/docs/modules/ROOT/pages/servlet/test/mockmvc/result-handlers.adoc +++ b/docs/modules/ROOT/pages/servlet/test/mockmvc/result-handlers.adoc @@ -1,4 +1,4 @@ -= SecurityMockMvcResultHandlers +=== SecurityMockMvcResultHandlers Spring Security provides a few ``ResultHandler``s implementations. In order to use Spring Security's ``ResultHandler``s implementations ensure the following static import is used: @@ -8,7 +8,7 @@ In order to use Spring Security's ``ResultHandler``s implementations ensure the import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultHandlers.*; ---- -== Exporting the SecurityContext +==== Exporting the SecurityContext Often times we want to query a repository to see if some `MockMvc` request actually persisted in the database. In some cases our repository query uses the xref:features/integrations/data.adoc[Spring Data Integration] to filter the results based on current user's username or any other property. diff --git a/docs/modules/ROOT/pages/servlet/test/mockmvc/result-matchers.adoc b/docs/modules/ROOT/pages/servlet/test/mockmvc/result-matchers.adoc index 40a2a69d48..8571418584 100644 --- a/docs/modules/ROOT/pages/servlet/test/mockmvc/result-matchers.adoc +++ b/docs/modules/ROOT/pages/servlet/test/mockmvc/result-matchers.adoc @@ -1,4 +1,4 @@ -= SecurityMockMvcResultMatchers +== SecurityMockMvcResultMatchers At times it is desirable to make various security related assertions about a request. To accommodate this need, Spring Security Test support implements Spring MVC Test's `ResultMatcher` interface. @@ -22,7 +22,7 @@ import org.springframework.security.test.web.servlet.response.SecurityMockMvcRes ---- ====== -== Unauthenticated Assertion +=== Unauthenticated Assertion At times it may be valuable to assert that there is no authenticated user associated with the result of a `MockMvc` invocation. For example, you might want to test submitting an invalid username and password and verify that no user is authenticated. @@ -49,7 +49,7 @@ mvc ---- ====== -== Authenticated Assertion +=== Authenticated Assertion It is often times that we must assert that an authenticated user exists. For example, we may want to verify that we authenticated successfully. diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index ebcc9eda06..49eacc7c9a 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -1,13 +1,65 @@ [[new]] -= What's New in Spring Security 6.2 += What's New in Spring Security 6.0 -Spring Security 6.2 provides a number of new features. +Spring Security 6.0 provides a number of new features. Below are the highlights of the release. -== Configuration +== Baseline Changes -* https://github.com/spring-projects/spring-security/issues/5011[gh-5011] - xref:servlet/integrations/cors.adoc[(docs)] Automatically enable `.cors()` if `CorsConfigurationSource` bean is present -* https://github.com/spring-projects/spring-security/issues/13204[gh-13204] - xref:migration-7/configuration.adoc#_use_with_instead_of_apply_for_custom_dsls[(docs)] Add `AbstractConfiguredSecurityBuilder.with(...)` method to apply configurers returning the builder -* https://github.com/spring-projects/spring-security/pull/13587[gh-13587] - https://spring.io/blog/2023/08/22/tackling-the-oauth2-client-component-model-in-spring-security/[blog post] Simplify configuration of OAuth2 Client component model -* https://github.com/spring-projects/spring-security/issues/7845[gh-7845] - xref:reactive/oauth2/login/logout.adoc#configure-provider-initiated-oidc-logout[docs] Add OIDC Back-channel Logout Support -* https://github.com/spring-projects/spring-security/pull/13857[gh-13857] - xref:servlet/authorization/authorize-http-requests.adoc#match-by-mvc[docs] Add servlet pattern support to AuthorizeHttpRequests +* Spring Security 6 requires JDK 17 + +== Breaking Changes + +* https://github.com/spring-projects/spring-security/issues/8980[gh-8980] - Remove unsafe/deprecated `Encryptors.querableText(CharSequence,CharSequence)`. +Instead use data storage to encrypt values. +* https://github.com/spring-projects/spring-security/issues/11520[gh-11520] - Remember Me uses SHA256 by default +* https://github.com/spring-projects/spring-security/issues/8819[gh-8819] - Move filters to web package +Reorganize imports +* https://github.com/spring-projects/spring-security/issues/7349[gh-7349] - Move filter and token to appropriate packages +Reorganize imports +* https://github.com/spring-projects/spring-security/issues/11026[gh-11026] - Use `RequestAttributeSecurityContextRepository` instead of `NullSecurityContextRepository` +* https://github.com/spring-projects/spring-security/pull/11887[gh-11827] - Change default authority for `oauth2Login()` +* https://github.com/spring-projects/spring-security/issues/10347[gh-10347] - Remove `UsernamePasswordAuthenticationToken` check in `BasicAuthenticationFilter` +* https://github.com/spring-projects/spring-security/pull/11923[gh-11923] - Remove `WebSecurityConfigurerAdapter`. +Instead, create a https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter[SecurityFilterChain bean]. +* https://github.com/spring-projects/spring-security/issues/11899[gh-11899] - Use `MvcRequestMatcher` by default if Spring MVC is present. +You can configure a different `RequestMatcher` by using the https://docs.spring.io/spring-security/reference/servlet/appendix/namespace/http.html#nsa-http-attributes[request-matcher attribute from ]. +* Change use-authorization-manager="true" to default +If the application uses `use-expressions="true"` or `access-decision-manager-ref` switch to `use-expressions="false"` or `authorization-manager-ref`, respectively. +If application relies on the implicit ``, this is no longer implicit and needs to be specified. +Or use `use-authorization-manager="false"` +* https://github.com/spring-projects/spring-security/issues/11939[gh-11939] - Remove deprecated `antMatchers`, `mvcMatchers`, `regexMatchers` helper methods from Java Configuration. +Instead, use `requestMatchers` or `HttpSecurity#securityMatchers`. +* https://github.com/spring-projects/spring-security/issues/11985[gh-11985] - Remove deprecated constructors in `Argon2PasswordEncoder`, `SCryptPasswordEncoder` and `Pbkdf2PasswordEncoder`. +* https://github.com/spring-projects/spring-security/issues/11960[gh-11960] - Default to Xor CSRF protection for xref:servlet/exploits/csrf.adoc#servlet-csrf-configure-request-handler[servlet] and xref:reactive/exploits/csrf.adoc#webflux-csrf-configure-request-handler[reactive] +* https://github.com/spring-projects/spring-security/issues/12019[gh-12019] - Remove deprecated method `setTokenFromMultipartDataEnabled` from `CsrfWebFilter` +* https://github.com/spring-projects/spring-security/issues/12020[gh-12020] - Remove deprecated method `tokenFromMultipartDataEnabled` from Java Configuration +* https://github.com/spring-projects/spring-security/issues/9429[gh-9429] - `Authentication(Web)Filter` rethrows `AuthenticationServiceException`s +* https://github.com/spring-projects/spring-security/issues/11027[gh-11027], https://github.com/spring-projects/spring-security/issues/11466[gh-11466] - Authorization on every dispatcher type +* https://github.com/spring-projects/spring-security/issues/11110[gh-11110] - Require explicit session saves by default +* https://github.com/spring-projects/spring-security/issues/11057[gh-11057] - Remove `MessageSourceAware` from `ExceptionTranslationWebFilter` +* https://github.com/spring-projects/spring-security/issues/12022[gh-12202] - Remove OAuth deprecations +* https://github.com/spring-projects/spring-security/issues/10556[gh-10556] - Remove EOL OpenSaml 3 Support. +Use the OpenSaml 4 Support instead. +* https://github.com/spring-projects/spring-security/issues/11077[gh-11077] - Remove SAML deprecations +** Remove `Converter` constructors from `Saml2MetadataFilter` and `Saml2AuthenticationTokenConverter` +** Remove `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory` and implementations +** Remove `Saml2AuthenticationToken(String, String, String, String, List)` +** Remove `RelyingPartyRegistration.ProviderDetails` and related methods +** Remove `OpenSamlAuthenticationProvider` +* https://github.com/spring-projects/spring-security/issues/12180[gh-12180] - Register `FilterChainProxy` for all dispatcher types + +== Core + +* https://github.com/spring-projects/spring-security/issues/11446[gh-11446] - Add native image support for `@PreAuthorize` +* https://github.com/spring-projects/spring-security/issues/11737[gh-11737] - Add native image support for `@PostAuthorize` +* xref:servlet/integrations/observability.adoc[Instrumentation] of `AuthenticationManager`, `AuthorizationManager`, and `FilterChainProxy` +* xref:reactive/integrations/observability.adoc[Instrumentation] of `ReactiveAuthenticationManager`, `ReactiveAuthorizationManager`, and `WebFilterChainProxy` + +== LDAP + +* https://github.com/spring-projects/spring-security/pull/9276[gh-9276] - LdapAuthoritiesPopulator is post-processed + +== Web + +* https://github.com/spring-projects/spring-security/issues/11432[gh-11432] - `CookieServerCsrfTokenRepository` supports maxage diff --git a/docs/spring-security-docs.gradle b/docs/spring-security-docs.gradle index 5112f6be5b..8b199c0850 100644 --- a/docs/spring-security-docs.gradle +++ b/docs/spring-security-docs.gradle @@ -49,7 +49,6 @@ def generateAttributes() { def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/" def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/" def springBootReferenceUrl = "https://docs.spring.io/spring-boot/docs/$springBootVersion/reference/html/" - def springBootApiUrl = "https://docs.spring.io/spring-boot/docs/$springBootVersion/api/" return ['gh-old-samples-url': ghOldSamplesUrl.toString(), 'gh-samples-url': ghSamplesUrl.toString(), @@ -58,7 +57,6 @@ def generateAttributes() { 'security-reference-url': securityReferenceUrl.toString(), 'spring-framework-api-url': springFrameworkApiUrl.toString(), 'spring-framework-reference-url': springFrameworkReferenceUrl.toString(), - 'spring-boot-api-url': springBootApiUrl.toString(), 'spring-boot-reference-url': springBootReferenceUrl.toString(), 'spring-security-version': project.version] + resolvedVersions(project.configurations.testRuntimeClasspath) diff --git a/etc/nohttp/allowlist.lines b/etc/nohttp/allowlist.lines index 330ed0f5ce..a378625640 100644 --- a/etc/nohttp/allowlist.lines +++ b/etc/nohttp/allowlist.lines @@ -10,5 +10,4 @@ ^http://www.w3.org/2001/04/xmlenc ^http://www.springframework.org/schema/security/.* ^http://openoffice.org/.* -^http://www.w3.org/2003/g/data-view -^http://schemas.openid.net/event/backchannel-logout +^http://www.w3.org/2003/g/data-view \ No newline at end of file diff --git a/git/hooks/.forward-merge.swp b/git/hooks/.forward-merge.swp deleted file mode 100644 index 9fb46808e50407f0e60c8697ff796dc6afc21141..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 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 Uz> record, String attributeName, String... values) { - assertThat(record).containsKey(attributeName); + assertThat(record.containsKey(attributeName)).isTrue(); assertThat(record.get(attributeName)).hasSize(values.length); for (int i = 0; i < values.length; i++) { assertThat(record.get(attributeName).get(i)).isEqualTo(values[i]); diff --git a/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulatorGetGrantedAuthoritiesTests.java b/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulatorGetGrantedAuthoritiesTests.java index 95181b7651..9321ef16c6 100644 --- a/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulatorGetGrantedAuthoritiesTests.java +++ b/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulatorGetGrantedAuthoritiesTests.java @@ -72,8 +72,8 @@ public class DefaultLdapAuthoritiesPopulatorGetGrantedAuthoritiesTests { assertThat(authorities).as("Should have 1 role").hasSize(2); - assertThat(authorities).contains("ROLE_DEVELOPER"); - assertThat(authorities).contains("ROLE_"); + assertThat(authorities.contains("ROLE_DEVELOPER")).isTrue(); + assertThat(authorities.contains("ROLE_")).isTrue(); } @Configuration diff --git a/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulatorTests.java b/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulatorTests.java index 1d6e2d6664..cef1e20c08 100644 --- a/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulatorTests.java +++ b/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulatorTests.java @@ -68,7 +68,7 @@ public class DefaultLdapAuthoritiesPopulatorTests { Collection authorities = this.populator.getGrantedAuthorities(ctx, "notfound"); assertThat(authorities).hasSize(1); - assertThat(AuthorityUtils.authorityListToSet(authorities)).contains("ROLE_USER"); + assertThat(AuthorityUtils.authorityListToSet(authorities).contains("ROLE_USER")).isTrue(); } @Test @@ -79,7 +79,7 @@ public class DefaultLdapAuthoritiesPopulatorTests { Collection authorities = this.populator .getGrantedAuthorities(new DirContextAdapter(new DistinguishedName("cn=notused")), "notused"); assertThat(authorities).hasSize(1); - assertThat(AuthorityUtils.authorityListToSet(authorities)).contains("ROLE_USER"); + assertThat(AuthorityUtils.authorityListToSet(authorities).contains("ROLE_USER")).isTrue(); } @Test @@ -98,8 +98,8 @@ public class DefaultLdapAuthoritiesPopulatorTests { assertThat(authorities).as("Should have 2 roles").hasSize(2); - assertThat(authorities).contains("ROLE_DEVELOPER"); - assertThat(authorities).contains("ROLE_MANAGER"); + assertThat(authorities.contains("ROLE_DEVELOPER")).isTrue(); + assertThat(authorities.contains("ROLE_MANAGER")).isTrue(); } @Test @@ -115,7 +115,7 @@ public class DefaultLdapAuthoritiesPopulatorTests { .authorityListToSet(this.populator.getGrantedAuthorities(ctx, "manager")); assertThat(authorities).as("Should have 1 role").hasSize(1); - assertThat(authorities).contains("ROLE_MANAGER"); + assertThat(authorities.contains("ROLE_MANAGER")).isTrue(); } @Test @@ -130,8 +130,8 @@ public class DefaultLdapAuthoritiesPopulatorTests { .authorityListToSet(this.populator.getGrantedAuthorities(ctx, "manager")); assertThat(authorities).as("Should have 2 roles").hasSize(2); - assertThat(authorities).contains("ROLE_MANAGER"); - assertThat(authorities).contains("ROLE_DEVELOPER"); + assertThat(authorities.contains("ROLE_MANAGER")).isTrue(); + assertThat(authorities.contains("ROLE_DEVELOPER")).isTrue(); } @Test @@ -147,9 +147,9 @@ public class DefaultLdapAuthoritiesPopulatorTests { .authorityListToSet(this.populator.getGrantedAuthorities(ctx, "manager")); assertThat(authorities).as("Should have 3 roles").hasSize(3); - assertThat(authorities).contains("ROLE_MANAGER"); - assertThat(authorities).contains("ROLE_SUBMANAGER"); - assertThat(authorities).contains("ROLE_DEVELOPER"); + assertThat(authorities.contains("ROLE_MANAGER")).isTrue(); + assertThat(authorities.contains("ROLE_SUBMANAGER")).isTrue(); + assertThat(authorities.contains("ROLE_DEVELOPER")).isTrue(); } @Test @@ -164,7 +164,7 @@ public class DefaultLdapAuthoritiesPopulatorTests { Collection authorities = this.populator .getGrantedAuthorities(new DirContextAdapter(new DistinguishedName("cn=notused")), "notused"); assertThat(authorities).hasSize(1); - assertThat(AuthorityUtils.authorityListToSet(authorities)).contains("ROLE_EXTRA"); + assertThat(AuthorityUtils.authorityListToSet(authorities).contains("ROLE_EXTRA")).isTrue(); } @Test @@ -180,7 +180,7 @@ public class DefaultLdapAuthoritiesPopulatorTests { .authorityListToSet(this.populator.getGrantedAuthorities(ctx, "notused")); assertThat(authorities).as("Should have 1 role").hasSize(1); - assertThat(authorities).contains("ROLE_MANAGER"); + assertThat(authorities.contains("ROLE_MANAGER")).isTrue(); } @Test diff --git a/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulatorTests.java b/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulatorTests.java index aec53f235c..beed1a45ba 100644 --- a/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulatorTests.java +++ b/ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulatorTests.java @@ -128,14 +128,14 @@ public class NestedLdapAuthoritiesPopulatorTests { LdapAuthority[] ldapAuthorities = authorities.toArray(new LdapAuthority[0]); assertThat(ldapAuthorities).hasSize(5); // groovy-developers group - assertThat(ldapAuthorities[0].getAttributes()).containsKey("member"); + assertThat(ldapAuthorities[0].getAttributes().containsKey("member")).isTrue(); assertThat(ldapAuthorities[0].getAttributes().get("member")).isNotNull(); assertThat(ldapAuthorities[0].getAttributes().get("member")).hasSize(3); assertThat(ldapAuthorities[0].getFirstAttributeValue("member")) .isEqualTo("cn=groovy-developers,ou=jdeveloper,dc=springframework,dc=org"); // java group - assertThat(ldapAuthorities[1].getAttributes()).containsKey("member"); + assertThat(ldapAuthorities[1].getAttributes().containsKey("member")).isTrue(); assertThat(ldapAuthorities[1].getAttributes().get("member")).isNotNull(); assertThat(ldapAuthorities[1].getAttributes().get("member")).hasSize(3); assertThat(this.groovyDevelopers.getDn()).isEqualTo(ldapAuthorities[1].getFirstAttributeValue("member")); diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java index d04e13d7b4..4dcb42aee0 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java @@ -260,30 +260,44 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda String hexString = Integer.toHexString(code); Throwable cause = new ActiveDirectoryAuthenticationException(hexString, exception.getMessage(), exception); switch (code) { - case PASSWORD_EXPIRED -> throw new CredentialsExpiredException(this.messages - .getMessage("LdapAuthenticationProvider.credentialsExpired", "User credentials have expired"), cause); - case ACCOUNT_DISABLED -> throw new DisabledException( - this.messages.getMessage("LdapAuthenticationProvider.disabled", "User is disabled"), cause); - case ACCOUNT_EXPIRED -> throw new AccountExpiredException( - this.messages.getMessage("LdapAuthenticationProvider.expired", "User account has expired"), cause); - case ACCOUNT_LOCKED -> throw new LockedException( - this.messages.getMessage("LdapAuthenticationProvider.locked", "User account is locked"), cause); - default -> throw badCredentials(cause); + case PASSWORD_EXPIRED: + throw new CredentialsExpiredException(this.messages.getMessage( + "LdapAuthenticationProvider.credentialsExpired", "User credentials have expired"), cause); + case ACCOUNT_DISABLED: + throw new DisabledException( + this.messages.getMessage("LdapAuthenticationProvider.disabled", "User is disabled"), cause); + case ACCOUNT_EXPIRED: + throw new AccountExpiredException( + this.messages.getMessage("LdapAuthenticationProvider.expired", "User account has expired"), + cause); + case ACCOUNT_LOCKED: + throw new LockedException( + this.messages.getMessage("LdapAuthenticationProvider.locked", "User account is locked"), cause); + default: + throw badCredentials(cause); } } private String subCodeToLogMessage(int code) { - return switch (code) { - case USERNAME_NOT_FOUND -> "User was not found in directory"; - case INVALID_PASSWORD -> "Supplied password was invalid"; - case NOT_PERMITTED -> "User not permitted to logon at this time"; - case PASSWORD_EXPIRED -> "Password has expired"; - case ACCOUNT_DISABLED -> "Account is disabled"; - case ACCOUNT_EXPIRED -> "Account expired"; - case PASSWORD_NEEDS_RESET -> "User must reset password"; - case ACCOUNT_LOCKED -> "Account locked"; - default -> "Unknown (error code " + Integer.toHexString(code) + ")"; - }; + switch (code) { + case USERNAME_NOT_FOUND: + return "User was not found in directory"; + case INVALID_PASSWORD: + return "Supplied password was invalid"; + case NOT_PERMITTED: + return "User not permitted to logon at this time"; + case PASSWORD_EXPIRED: + return "Password has expired"; + case ACCOUNT_DISABLED: + return "Account is disabled"; + case ACCOUNT_EXPIRED: + return "Account expired"; + case PASSWORD_NEEDS_RESET: + return "User must reset password"; + case ACCOUNT_LOCKED: + return "Account locked"; + } + return "Unknown (error code " + Integer.toHexString(code) + ")"; } private BadCredentialsException badCredentials() { diff --git a/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java b/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java index 1fe397ac51..379faed965 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java +++ b/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java @@ -73,10 +73,10 @@ import org.springframework.util.Assert; * @author Rob Winch * @author Gunnar Hillert * @author Evgeniy Cheban - * @deprecated For removal in 7.0. Use {@link UnboundIdContainer} instead because ApacheDS - * 1.x is no longer supported with no GA version to replace it. + * @deprecated Use {@link UnboundIdContainer} instead because ApacheDS 1.x is no longer + * supported with no GA version to replace it. */ -@Deprecated(since = "5.2", forRemoval = true) +@Deprecated public class ApacheDSContainer implements EmbeddedLdapServerContainer, InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapAuthority.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapAuthority.java index e08678b2a4..9e89a5d631 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapAuthority.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapAuthority.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2014 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. @@ -113,13 +113,14 @@ public class LdapAuthority implements GrantedAuthority { if (this == obj) { return true; } - if (!(obj instanceof LdapAuthority other)) { + if (!(obj instanceof LdapAuthority)) { return false; } - if (!this.dn.equals(other.getDn())) { + LdapAuthority other = (LdapAuthority) obj; + if (!this.dn.equals(other.dn)) { return false; } - return this.role.equals(other.getAuthority()); + return this.role.equals(other.role); } @Override diff --git a/ldap/src/test/java/org/springframework/security/ldap/userdetails/InetOrgPersonTests.java b/ldap/src/test/java/org/springframework/security/ldap/userdetails/InetOrgPersonTests.java index a572c61765..a1279c3471 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/userdetails/InetOrgPersonTests.java +++ b/ldap/src/test/java/org/springframework/security/ldap/userdetails/InetOrgPersonTests.java @@ -46,7 +46,7 @@ public class InetOrgPersonTests { InetOrgPerson p2 = (InetOrgPerson) essence.createUserDetails(); Set set = new HashSet<>(); set.add(p); - assertThat(set).contains(p2); + assertThat(set.contains(p2)).isTrue(); } @Test diff --git a/ldap/src/test/java/org/springframework/security/ldap/userdetails/LdapUserDetailsServiceTests.java b/ldap/src/test/java/org/springframework/security/ldap/userdetails/LdapUserDetailsServiceTests.java index a9007ad052..b9d2ffa45e 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/userdetails/LdapUserDetailsServiceTests.java +++ b/ldap/src/test/java/org/springframework/security/ldap/userdetails/LdapUserDetailsServiceTests.java @@ -60,7 +60,7 @@ public class LdapUserDetailsServiceTests { UserDetails user = service.loadUserByUsername("doesntmatterwegetjoeanyway"); Set authorities = AuthorityUtils.authorityListToSet(user.getAuthorities()); assertThat(authorities).hasSize(1); - assertThat(authorities).contains("ROLE_FROM_POPULATOR"); + assertThat(authorities.contains("ROLE_FROM_POPULATOR")).isTrue(); } @Test diff --git a/ldap/src/test/java/org/springframework/security/ldap/userdetails/UserDetailsServiceLdapAuthoritiesPopulatorTests.java b/ldap/src/test/java/org/springframework/security/ldap/userdetails/UserDetailsServiceLdapAuthoritiesPopulatorTests.java index 486dda34c2..b2912790af 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/userdetails/UserDetailsServiceLdapAuthoritiesPopulatorTests.java +++ b/ldap/src/test/java/org/springframework/security/ldap/userdetails/UserDetailsServiceLdapAuthoritiesPopulatorTests.java @@ -47,7 +47,7 @@ public class UserDetailsServiceLdapAuthoritiesPopulatorTests { UserDetailsServiceLdapAuthoritiesPopulator populator = new UserDetailsServiceLdapAuthoritiesPopulator(uds); Collection auths = populator.getGrantedAuthorities(new DirContextAdapter(), "joe"); assertThat(auths).hasSize(1); - assertThat(AuthorityUtils.authorityListToSet(auths)).contains("ROLE_USER"); + assertThat(AuthorityUtils.authorityListToSet(auths).contains("ROLE_USER")).isTrue(); } } diff --git a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.java b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.java index d442e7532c..68c1be5d7b 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.java +++ b/messaging/src/main/java/org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.java @@ -85,7 +85,8 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho if (!matcher.matches((Message) message)) { return null; } - if (matcher instanceof SimpDestinationMessageMatcher simp) { + if (matcher instanceof SimpDestinationMessageMatcher) { + SimpDestinationMessageMatcher simp = (SimpDestinationMessageMatcher) matcher; return new MessageAuthorizationContext<>(message, simp.extractPathVariables(message)); } if (matcher instanceof Builder.LazySimpDestinationMessageMatcher) { @@ -110,7 +111,7 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho private final List>>> mappings = new ArrayList<>(); - private Supplier pathMatcher = AntPathMatcher::new; + private Supplier pathMatcher = () -> new AntPathMatcher(); public Builder() { } diff --git a/messaging/src/main/java/org/springframework/security/messaging/context/SecurityContextPropagationChannelInterceptor.java b/messaging/src/main/java/org/springframework/security/messaging/context/SecurityContextPropagationChannelInterceptor.java deleted file mode 100644 index be68265948..0000000000 --- a/messaging/src/main/java/org/springframework/security/messaging/context/SecurityContextPropagationChannelInterceptor.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2002-2023 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.messaging.context; - -import java.util.Stack; - -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.simp.SimpMessageHeaderAccessor; -import org.springframework.messaging.support.ExecutorChannelInterceptor; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.context.SecurityContextHolderStrategy; -import org.springframework.util.Assert; - -/** - * An {@link ExecutorChannelInterceptor} that takes an {@link Authentication} from the - * current {@link SecurityContext} (if any) in the - * {@link #preSend(Message, MessageChannel)} callback and stores it into an - * {@link #authenticationHeaderName} message header. Then sets the context from this - * header in the {@link #beforeHandle(Message, MessageChannel, MessageHandler)} and - * {@link #postReceive(Message, MessageChannel)} both of which typically happen on a - * different thread. - *

      - * Note: cannot be used in combination with a {@link SecurityContextChannelInterceptor} on - * the same channel since both these interceptors modify a security context on a handling - * and receiving operations. - * - * @author Artem Bilan - * @since 6.2 - * @see SecurityContextChannelInterceptor - */ -public final class SecurityContextPropagationChannelInterceptor implements ExecutorChannelInterceptor { - - private static final ThreadLocal> originalContext = new ThreadLocal<>(); - - private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder - .getContextHolderStrategy(); - - private SecurityContext empty = this.securityContextHolderStrategy.createEmptyContext(); - - private final String authenticationHeaderName; - - private Authentication anonymous = new AnonymousAuthenticationToken("key", "anonymous", - AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); - - /** - * Create a new instance using the header of the name - * {@link SimpMessageHeaderAccessor#USER_HEADER}. - */ - public SecurityContextPropagationChannelInterceptor() { - this(SimpMessageHeaderAccessor.USER_HEADER); - } - - /** - * Create a new instance that uses the specified header to populate the - * {@link Authentication}. - * @param authenticationHeaderName the header name to populate the - * {@link Authentication}. Cannot be null. - */ - public SecurityContextPropagationChannelInterceptor(String authenticationHeaderName) { - Assert.notNull(authenticationHeaderName, "authenticationHeaderName cannot be null"); - this.authenticationHeaderName = authenticationHeaderName; - } - - public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strategy) { - this.securityContextHolderStrategy = strategy; - this.empty = this.securityContextHolderStrategy.createEmptyContext(); - } - - /** - * Configure an Authentication used for anonymous authentication. Default is:

      -	 * new AnonymousAuthenticationToken("key", "anonymous",
      -	 * 		AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
      -	 * 
      - * @param authentication the Authentication used for anonymous authentication. Cannot - * be null. - */ - public void setAnonymousAuthentication(Authentication authentication) { - Assert.notNull(authentication, "authentication cannot be null"); - this.anonymous = authentication; - } - - @Override - public Message preSend(Message message, MessageChannel channel) { - Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication(); - if (authentication == null) { - authentication = this.anonymous; - } - return MessageBuilder.fromMessage(message).setHeader(this.authenticationHeaderName, authentication).build(); - } - - @Override - public Message beforeHandle(Message message, MessageChannel channel, MessageHandler handler) { - return postReceive(message, channel); - } - - @Override - public Message postReceive(Message message, MessageChannel channel) { - setup(message); - return message; - } - - @Override - public void afterMessageHandled(Message message, MessageChannel channel, MessageHandler handler, Exception ex) { - cleanup(); - } - - private void setup(Message message) { - Authentication authentication = message.getHeaders().get(this.authenticationHeaderName, Authentication.class); - SecurityContext currentContext = this.securityContextHolderStrategy.getContext(); - Stack contextStack = originalContext.get(); - if (contextStack == null) { - contextStack = new Stack<>(); - originalContext.set(contextStack); - } - contextStack.push(currentContext); - SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); - context.setAuthentication(authentication); - this.securityContextHolderStrategy.setContext(context); - } - - private void cleanup() { - Stack contextStack = originalContext.get(); - if (contextStack == null || contextStack.isEmpty()) { - this.securityContextHolderStrategy.clearContext(); - originalContext.remove(); - return; - } - SecurityContext context = contextStack.pop(); - try { - if (this.empty.equals(context)) { - this.securityContextHolderStrategy.clearContext(); - originalContext.remove(); - } - else { - this.securityContextHolderStrategy.setContext(context); - } - } - catch (Throwable ex) { - this.securityContextHolderStrategy.clearContext(); - } - } - -} diff --git a/messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpMessageTypeMatcher.java b/messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpMessageTypeMatcher.java index 2b2e2cfae5..00aa6e8a64 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpMessageTypeMatcher.java +++ b/messaging/src/main/java/org/springframework/security/messaging/util/matcher/SimpMessageTypeMatcher.java @@ -57,9 +57,10 @@ public class SimpMessageTypeMatcher implements MessageMatcher { if (this == other) { return true; } - if (!(other instanceof SimpMessageTypeMatcher otherMatcher)) { + if (!(other instanceof SimpMessageTypeMatcher)) { return false; } + SimpMessageTypeMatcher otherMatcher = (SimpMessageTypeMatcher) other; return ObjectUtils.nullSafeEquals(this.typeToMatch, otherMatcher.typeToMatch); } diff --git a/messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextPropagationChannelInterceptorTests.java b/messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextPropagationChannelInterceptorTests.java deleted file mode 100644 index 1682b96200..0000000000 --- a/messaging/src/test/java/org/springframework/security/messaging/context/SecurityContextPropagationChannelInterceptorTests.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2002-2023 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.messaging.context; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.simp.SimpMessageHeaderAccessor; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.context.SecurityContextHolderStrategy; -import org.springframework.security.core.context.SecurityContextImpl; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -@ExtendWith(MockitoExtension.class) -public class SecurityContextPropagationChannelInterceptorTests { - - @Mock - MessageChannel channel; - - @Mock - MessageHandler handler; - - MessageBuilder messageBuilder; - - Authentication authentication; - - SecurityContextPropagationChannelInterceptor interceptor; - - @BeforeEach - public void setup() { - this.authentication = new TestingAuthenticationToken("user", "pass", "ROLE_USER"); - this.messageBuilder = MessageBuilder.withPayload("payload"); - this.interceptor = new SecurityContextPropagationChannelInterceptor(); - } - - @AfterEach - public void cleanup() { - this.interceptor.afterMessageHandled(this.messageBuilder.build(), this.channel, this.handler, null); - SecurityContextHolder.clearContext(); - } - - @Test - public void preSendDefaultHeader() { - SecurityContextHolder.getContext().setAuthentication(this.authentication); - Message message = this.interceptor.preSend(this.messageBuilder.build(), this.channel); - assertThat(message.getHeaders()).containsEntry(SimpMessageHeaderAccessor.USER_HEADER, this.authentication); - } - - @Test - public void preSendCustomHeader() { - SecurityContextHolder.getContext().setAuthentication(this.authentication); - String headerName = "header"; - this.interceptor = new SecurityContextPropagationChannelInterceptor(headerName); - Message message = this.interceptor.preSend(this.messageBuilder.build(), this.channel); - assertThat(message.getHeaders()).containsEntry(headerName, this.authentication); - } - - @Test - public void preSendWhenCustomSecurityContextHolderStrategyThenUserSet() { - SecurityContextHolderStrategy strategy = spy(SecurityContextHolder.getContextHolderStrategy()); - strategy.setContext(new SecurityContextImpl(this.authentication)); - this.interceptor.setSecurityContextHolderStrategy(strategy); - Message message = this.interceptor.preSend(this.messageBuilder.build(), this.channel); - this.interceptor.beforeHandle(message, this.channel, this.handler); - verify(strategy, times(2)).getContext(); - assertThat(strategy.getContext().getAuthentication()).isSameAs(this.authentication); - } - - @Test - public void preSendUserNoContext() { - Message message = this.interceptor.preSend(this.messageBuilder.build(), this.channel); - assertThat(message.getHeaders()).containsKey(SimpMessageHeaderAccessor.USER_HEADER); - assertThat(message.getHeaders().get(SimpMessageHeaderAccessor.USER_HEADER)) - .isInstanceOf(AnonymousAuthenticationToken.class); - } - - @Test - public void beforeHandleUserSet() { - this.messageBuilder.setHeader(SimpMessageHeaderAccessor.USER_HEADER, this.authentication); - this.interceptor.beforeHandle(this.messageBuilder.build(), this.channel, this.handler); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.authentication); - } - - @Test - public void postReceiveUserSet() { - this.messageBuilder.setHeader(SimpMessageHeaderAccessor.USER_HEADER, this.authentication); - this.interceptor.postReceive(this.messageBuilder.build(), this.channel); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.authentication); - } - - @Test - public void authenticationIsPropagatedFromPreSendToPostReceive() { - SecurityContextHolder.getContext().setAuthentication(this.authentication); - Message message = this.interceptor.preSend(this.messageBuilder.build(), this.channel); - assertThat(message.getHeaders().get(SimpMessageHeaderAccessor.USER_HEADER)).isSameAs(this.authentication); - this.interceptor.postReceive(message, this.channel); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.authentication); - } - - @Test - public void beforeHandleUserNotSet() { - this.interceptor.beforeHandle(this.messageBuilder.build(), this.channel, this.handler); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); - } - - @Test - public void afterMessageHandledUserNotSet() { - this.interceptor.afterMessageHandled(this.messageBuilder.build(), this.channel, this.handler, null); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); - } - - @Test - public void afterMessageHandled() { - SecurityContextHolder.getContext().setAuthentication(this.authentication); - this.messageBuilder.setHeader(SimpMessageHeaderAccessor.USER_HEADER, this.authentication); - this.interceptor.afterMessageHandled(this.messageBuilder.build(), this.channel, this.handler, null); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); - } - - @Test - public void restoresOriginalContext() { - TestingAuthenticationToken original = new TestingAuthenticationToken("original", "original", "ROLE_USER"); - SecurityContextHolder.getContext().setAuthentication(original); - this.messageBuilder.setHeader(SimpMessageHeaderAccessor.USER_HEADER, this.authentication); - this.interceptor.beforeHandle(this.messageBuilder.build(), this.channel, this.handler); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.authentication); - this.interceptor.afterMessageHandled(this.messageBuilder.build(), this.channel, this.handler, null); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(original); - } - -} diff --git a/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/ResolvableMethod.java b/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/ResolvableMethod.java index 3dd316660f..dbeaf95a62 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/ResolvableMethod.java +++ b/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/ResolvableMethod.java @@ -40,7 +40,7 @@ import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.Factory; import org.springframework.cglib.proxy.MethodProxy; -import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.MethodIntrospector; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; @@ -132,7 +132,7 @@ public final class ResolvableMethod { private static final SpringObjenesis objenesis = new SpringObjenesis(); - private static final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); + private static final ParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); // Matches ValueConstants.DEFAULT_NONE (spring-web and spring-messaging) private static final String DEFAULT_VALUE_NONE = "\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n"; diff --git a/messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java b/messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java index b13bdab5dc..608b3216ae 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/util/matcher/SimpDestinationMessageMatcherTests.java @@ -113,7 +113,7 @@ public class SimpDestinationMessageMatcherTests { this.matcher = new SimpDestinationMessageMatcher("/topics/{topic}/**"); this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/topics/someTopic/sub1"); this.messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.MESSAGE); - assertThat(this.matcher.extractPathVariables(this.messageBuilder.build())).containsEntry("topic", "someTopic"); + assertThat(this.matcher.extractPathVariables(this.messageBuilder.build()).get("topic")).isEqualTo("someTopic"); } @Test diff --git a/messaging/src/test/java/org/springframework/security/messaging/web/socket/server/CsrfTokenHandshakeInterceptorTests.java b/messaging/src/test/java/org/springframework/security/messaging/web/socket/server/CsrfTokenHandshakeInterceptorTests.java index 09ad468728..760fe3aa1b 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/web/socket/server/CsrfTokenHandshakeInterceptorTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/web/socket/server/CsrfTokenHandshakeInterceptorTests.java @@ -75,7 +75,7 @@ public class CsrfTokenHandshakeInterceptorTests { CsrfToken token = new DefaultCsrfToken("header", "param", "token"); this.httpRequest.setAttribute(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(token)); this.interceptor.beforeHandshake(this.request, this.response, this.wsHandler, this.attributes); - assertThat(this.attributes).containsOnlyKeys(CsrfToken.class.getName()); + assertThat(this.attributes.keySet()).containsOnly(CsrfToken.class.getName()); CsrfToken csrfToken = (CsrfToken) this.attributes.get(CsrfToken.class.getName()); assertThat(csrfToken.getHeaderName()).isEqualTo(token.getHeaderName()); assertThat(csrfToken.getParameterName()).isEqualTo(token.getParameterName()); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientService.java index 19e373fe72..f5847c1ad2 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientService.java @@ -396,7 +396,8 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient @Override protected void doSetValue(PreparedStatement ps, int parameterPosition, Object argValue) throws SQLException { - if (argValue instanceof SqlParameterValue paramValue) { + if (argValue instanceof SqlParameterValue) { + SqlParameterValue paramValue = (SqlParameterValue) argValue; if (paramValue.getSqlType() == Types.BLOB) { if (paramValue.getValue() != null) { Assert.isInstanceOf(byte[].class, paramValue.getValue(), diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RemoveAuthorizedClientOAuth2AuthorizationFailureHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RemoveAuthorizedClientOAuth2AuthorizationFailureHandler.java index 4f174b5c5e..5dc1896796 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RemoveAuthorizedClientOAuth2AuthorizationFailureHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RemoveAuthorizedClientOAuth2AuthorizationFailureHandler.java @@ -110,8 +110,9 @@ public class RemoveAuthorizedClientOAuth2AuthorizationFailureHandler implements @Override public void onAuthorizationFailure(OAuth2AuthorizationException authorizationException, Authentication principal, Map attributes) { - if (authorizationException instanceof ClientAuthorizationException clientAuthorizationException + if (authorizationException instanceof ClientAuthorizationException && hasRemovalErrorCode(authorizationException)) { + ClientAuthorizationException clientAuthorizationException = (ClientAuthorizationException) authorizationException; this.delegate.removeAuthorizedClient(clientAuthorizationException.getClientRegistrationId(), principal, attributes); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler.java index 0c92422234..0fd36f7f01 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler.java @@ -112,8 +112,9 @@ public class RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler @Override public Mono onAuthorizationFailure(OAuth2AuthorizationException authorizationException, Authentication principal, Map attributes) { - if (authorizationException instanceof ClientAuthorizationException clientAuthorizationException + if (authorizationException instanceof ClientAuthorizationException && hasRemovalErrorCode(authorizationException)) { + ClientAuthorizationException clientAuthorizationException = (ClientAuthorizationException) authorizationException; return this.delegate.removeAuthorizedClient(clientAuthorizationException.getClientRegistrationId(), principal, attributes); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java index 1ce0263431..8716ed874e 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java @@ -115,7 +115,8 @@ public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient String.format( "This class supports `client_secret_basic`, `client_secret_post`, and `none` by default. Client [%s] is using [%s] instead. Please use a supported client authentication method, or use `setRequestEntityConverter` to supply an instance that supports [%s].", - registrationId, clientAuthenticationMethod, clientAuthenticationMethod)); + registrationId, clientAuthenticationMethod.getValue(), clientAuthenticationMethod.getValue())); return this.delegate.convert(grantRequest); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/LogoutTokenClaimAccessor.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/LogoutTokenClaimAccessor.java deleted file mode 100644 index 49aeff4c3c..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/LogoutTokenClaimAccessor.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2002-2023 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.oidc.authentication.logout; - -import java.net.URL; -import java.time.Instant; -import java.util.List; -import java.util.Map; - -import org.springframework.security.oauth2.core.ClaimAccessor; - -/** - * A {@link ClaimAccessor} for the "claims" that can be returned in OIDC Logout - * Tokens - * - * @author Josh Cummings - * @since 6.2 - * @see OidcLogoutToken - * @see OIDC - * Back-Channel Logout Token - */ -public interface LogoutTokenClaimAccessor extends ClaimAccessor { - - /** - * Returns the Issuer identifier {@code (iss)}. - * @return the Issuer identifier - */ - default URL getIssuer() { - return this.getClaimAsURL(LogoutTokenClaimNames.ISS); - } - - /** - * Returns the Subject identifier {@code (sub)}. - * @return the Subject identifier - */ - default String getSubject() { - return this.getClaimAsString(LogoutTokenClaimNames.SUB); - } - - /** - * Returns the Audience(s) {@code (aud)} that this ID Token is intended for. - * @return the Audience(s) that this ID Token is intended for - */ - default List getAudience() { - return this.getClaimAsStringList(LogoutTokenClaimNames.AUD); - } - - /** - * Returns the time at which the ID Token was issued {@code (iat)}. - * @return the time at which the ID Token was issued - */ - default Instant getIssuedAt() { - return this.getClaimAsInstant(LogoutTokenClaimNames.IAT); - } - - /** - * Returns a {@link Map} that identifies this token as a logout token - * @return the identifying {@link Map} - */ - default Map getEvents() { - return getClaimAsMap(LogoutTokenClaimNames.EVENTS); - } - - /** - * Returns a {@code String} value {@code (sid)} representing the OIDC Provider session - * @return the value representing the OIDC Provider session - */ - default String getSessionId() { - return getClaimAsString(LogoutTokenClaimNames.SID); - } - - /** - * Returns the JWT ID {@code (jti)} claim which provides a unique identifier for the - * JWT. - * @return the JWT ID claim which provides a unique identifier for the JWT - */ - default String getId() { - return this.getClaimAsString(LogoutTokenClaimNames.JTI); - } - -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/LogoutTokenClaimNames.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/LogoutTokenClaimNames.java deleted file mode 100644 index 9893aa350a..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/LogoutTokenClaimNames.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2002-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.client.oidc.authentication.logout; - -/** - * The names of the "claims" defined by the OpenID Back-Channel Logout 1.0 - * specification that can be returned in a Logout Token. - * - * @author Josh Cummings - * @since 6.2 - * @see OidcLogoutToken - * @see OIDC - * Back-Channel Logout Token - */ -public final class LogoutTokenClaimNames { - - /** - * {@code jti} - the JTI identifier - */ - public static final String JTI = "jti"; - - /** - * {@code iss} - the Issuer identifier - */ - public static final String ISS = "iss"; - - /** - * {@code sub} - the Subject identifier - */ - public static final String SUB = "sub"; - - /** - * {@code aud} - the Audience(s) that the ID Token is intended for - */ - public static final String AUD = "aud"; - - /** - * {@code iat} - the time at which the ID Token was issued - */ - public static final String IAT = "iat"; - - /** - * {@code events} - a JSON object that identifies this token as a logout token - */ - public static final String EVENTS = "events"; - - /** - * {@code sid} - the session id for the OIDC provider - */ - public static final String SID = "sid"; - - private LogoutTokenClaimNames() { - } - -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcLogoutToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcLogoutToken.java deleted file mode 100644 index 41b425bf40..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcLogoutToken.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2002-2023 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.oidc.authentication.logout; - -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.function.Consumer; - -import org.springframework.security.oauth2.core.AbstractOAuth2Token; -import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; -import org.springframework.util.Assert; - -/** - * An implementation of an {@link AbstractOAuth2Token} representing an OpenID Backchannel - * Logout Token. - * - *

      - * The {@code OidcLogoutToken} is a security token that contains "claims" about - * terminating sessions for a given OIDC Provider session id or End User. - * - * @author Josh Cummings - * @since 6.2 - * @see AbstractOAuth2Token - * @see LogoutTokenClaimAccessor - * @see Logout - * Token - */ -public class OidcLogoutToken extends AbstractOAuth2Token implements LogoutTokenClaimAccessor { - - private static final String BACKCHANNEL_LOGOUT_TOKEN_EVENT_NAME = "http://schemas.openid.net/event/backchannel-logout"; - - private final Map claims; - - /** - * Constructs a {@link OidcLogoutToken} using the provided parameters. - * @param tokenValue the Logout Token value - * @param issuedAt the time at which the Logout Token was issued {@code (iat)} - * @param claims the claims about the logout statement - */ - OidcLogoutToken(String tokenValue, Instant issuedAt, Map claims) { - super(tokenValue, issuedAt, Instant.MAX); - this.claims = Collections.unmodifiableMap(claims); - Assert.notNull(claims, "claims must not be null"); - } - - @Override - public Map getClaims() { - return this.claims; - } - - /** - * Create a {@link OidcLogoutToken.Builder} based on the given token value - * @param tokenValue the token value to use - * @return the {@link OidcLogoutToken.Builder} for further configuration - */ - public static Builder withTokenValue(String tokenValue) { - return new Builder(tokenValue); - } - - /** - * A builder for {@link OidcLogoutToken}s - * - * @author Josh Cummings - */ - public static final class Builder { - - private String tokenValue; - - private final Map claims = new LinkedHashMap<>(); - - private Builder(String tokenValue) { - this.tokenValue = tokenValue; - this.claims.put(LogoutTokenClaimNames.EVENTS, - Collections.singletonMap(BACKCHANNEL_LOGOUT_TOKEN_EVENT_NAME, Collections.emptyMap())); - } - - /** - * Use this token value in the resulting {@link OidcLogoutToken} - * @param tokenValue The token value to use - * @return the {@link Builder} for further configurations - */ - public Builder tokenValue(String tokenValue) { - this.tokenValue = tokenValue; - return this; - } - - /** - * Use this claim in the resulting {@link OidcLogoutToken} - * @param name The claim name - * @param value The claim value - * @return the {@link Builder} for further configurations - */ - public Builder claim(String name, Object value) { - this.claims.put(name, value); - return this; - } - - /** - * Provides access to every {@link #claim(String, Object)} declared so far with - * the possibility to add, replace, or remove. - * @param claimsConsumer the consumer - * @return the {@link Builder} for further configurations - */ - public Builder claims(Consumer> claimsConsumer) { - claimsConsumer.accept(this.claims); - return this; - } - - /** - * Use this audience in the resulting {@link OidcLogoutToken} - * @param audience The audience(s) to use - * @return the {@link Builder} for further configurations - */ - public Builder audience(Collection audience) { - return claim(LogoutTokenClaimNames.AUD, audience); - } - - /** - * Use this issued-at timestamp in the resulting {@link OidcLogoutToken} - * @param issuedAt The issued-at timestamp to use - * @return the {@link Builder} for further configurations - */ - public Builder issuedAt(Instant issuedAt) { - return claim(LogoutTokenClaimNames.IAT, issuedAt); - } - - /** - * Use this issuer in the resulting {@link OidcLogoutToken} - * @param issuer The issuer to use - * @return the {@link Builder} for further configurations - */ - public Builder issuer(String issuer) { - return claim(LogoutTokenClaimNames.ISS, issuer); - } - - /** - * Use this id to identify the resulting {@link OidcLogoutToken} - * @param jti The unique identifier to use - * @return the {@link Builder} for further configurations - */ - public Builder jti(String jti) { - return claim(LogoutTokenClaimNames.JTI, jti); - } - - /** - * Use this subject in the resulting {@link OidcLogoutToken} - * @param subject The subject to use - * @return the {@link Builder} for further configurations - */ - public Builder subject(String subject) { - return claim(LogoutTokenClaimNames.SUB, subject); - } - - /** - * A JSON object that identifies this token as a logout token - * @param events The JSON object to use - * @return the {@link Builder} for further configurations - */ - public Builder events(Map events) { - return claim(LogoutTokenClaimNames.EVENTS, events); - } - - /** - * Use this session id to correlate the OIDC Provider session - * @param sessionId The session id to use - * @return the {@link Builder} for further configurations - */ - public Builder sessionId(String sessionId) { - return claim(LogoutTokenClaimNames.SID, sessionId); - } - - public OidcLogoutToken build() { - Assert.notNull(this.claims.get(LogoutTokenClaimNames.ISS), "issuer must not be null"); - Assert.isInstanceOf(Collection.class, this.claims.get(LogoutTokenClaimNames.AUD), - "audience must be a collection"); - Assert.notEmpty((Collection) this.claims.get(LogoutTokenClaimNames.AUD), "audience must not be empty"); - Assert.notNull(this.claims.get(LogoutTokenClaimNames.JTI), "jti must not be null"); - Assert.isTrue(hasLogoutTokenIdentifyingMember(), - "logout token must contain an events claim that contains a member called " + "'" - + BACKCHANNEL_LOGOUT_TOKEN_EVENT_NAME + "' whose value is an empty Map"); - Assert.isNull(this.claims.get("nonce"), "logout token must not contain a nonce claim"); - Instant iat = toInstant(this.claims.get(IdTokenClaimNames.IAT)); - return new OidcLogoutToken(this.tokenValue, iat, this.claims); - } - - private boolean hasLogoutTokenIdentifyingMember() { - if (!(this.claims.get(LogoutTokenClaimNames.EVENTS) instanceof Map events)) { - return false; - } - if (!(events.get(BACKCHANNEL_LOGOUT_TOKEN_EVENT_NAME) instanceof Map object)) { - return false; - } - return object.isEmpty(); - } - - private Instant toInstant(Object timestamp) { - if (timestamp != null) { - Assert.isInstanceOf(Instant.class, timestamp, "timestamps must be of type Instant"); - } - return (Instant) timestamp; - } - - } - -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/server/session/InMemoryReactiveOidcSessionRegistry.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/server/session/InMemoryReactiveOidcSessionRegistry.java deleted file mode 100644 index cd1e92ac93..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/server/session/InMemoryReactiveOidcSessionRegistry.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2002-2023 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.oidc.server.session; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; - -/** - * An in-memory implementation of - * {@link org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry} - * - * @author Josh Cummings - * @since 6.2 - */ -public final class InMemoryReactiveOidcSessionRegistry implements ReactiveOidcSessionRegistry { - - private final InMemoryOidcSessionRegistry delegate = new InMemoryOidcSessionRegistry(); - - @Override - public Mono saveSessionInformation(OidcSessionInformation info) { - this.delegate.saveSessionInformation(info); - return Mono.empty(); - } - - @Override - public Mono removeSessionInformation(String clientSessionId) { - return Mono.justOrEmpty(this.delegate.removeSessionInformation(clientSessionId)); - } - - @Override - public Flux removeSessionInformation(OidcLogoutToken token) { - return Flux.fromIterable(this.delegate.removeSessionInformation(token)); - } - -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/server/session/ReactiveOidcSessionRegistry.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/server/session/ReactiveOidcSessionRegistry.java deleted file mode 100644 index a4cbb39f46..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/server/session/ReactiveOidcSessionRegistry.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2002-2023 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.oidc.server.session; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; - -/** - * A registry to record the tie between the OIDC Provider session and the Client session. - * This is handy when a provider makes a logout request that indicates the OIDC Provider - * session or the End User. - * - * @author Josh Cummings - * @since 6.2 - * @see Logout - * Token - */ -public interface ReactiveOidcSessionRegistry { - - /** - * Register a OIDC Provider session with the provided client session. Generally - * speaking, the client session should be the session tied to the current login. - * @param info the {@link OidcSessionInformation} to use - */ - Mono saveSessionInformation(OidcSessionInformation info); - - /** - * Deregister the OIDC Provider session tied to the provided client session. Generally - * speaking, the client session should be the session tied to the current logout. - * @param clientSessionId the client session - * @return any found {@link OidcSessionInformation}, could be {@code null} - */ - Mono removeSessionInformation(String clientSessionId); - - /** - * Deregister the OIDC Provider sessions referenced by the provided OIDC Logout Token - * by its session id or its subject. Note that the issuer and audience should also - * match the corresponding values found in each {@link OidcSessionInformation} - * returned. - * @param logoutToken the {@link OidcLogoutToken} - * @return any found {@link OidcSessionInformation}s, could be empty - */ - Flux removeSessionInformation(OidcLogoutToken logoutToken); - -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistry.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistry.java deleted file mode 100644 index f5bb6235df..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistry.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2002-2023 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.oidc.session; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Predicate; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimNames; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; - -/** - * An in-memory implementation of {@link OidcSessionRegistry} - * - * @author Josh Cummings - * @since 6.2 - */ -public final class InMemoryOidcSessionRegistry implements OidcSessionRegistry { - - private final Log logger = LogFactory.getLog(InMemoryOidcSessionRegistry.class); - - private final Map sessions = new ConcurrentHashMap<>(); - - @Override - public void saveSessionInformation(OidcSessionInformation info) { - this.sessions.put(info.getSessionId(), info); - } - - @Override - public OidcSessionInformation removeSessionInformation(String clientSessionId) { - OidcSessionInformation information = this.sessions.remove(clientSessionId); - if (information != null) { - this.logger.trace("Removed client session"); - } - return information; - } - - @Override - public Iterable removeSessionInformation(OidcLogoutToken token) { - List audience = token.getAudience(); - String issuer = token.getIssuer().toString(); - String subject = token.getSubject(); - String providerSessionId = token.getSessionId(); - Predicate matcher = (providerSessionId != null) - ? sessionIdMatcher(audience, issuer, providerSessionId) : subjectMatcher(audience, issuer, subject); - if (this.logger.isTraceEnabled()) { - String message = "Looking up sessions by issuer [%s] and %s [%s]"; - if (providerSessionId != null) { - this.logger.trace(String.format(message, issuer, LogoutTokenClaimNames.SID, providerSessionId)); - } - else { - this.logger.trace(String.format(message, issuer, LogoutTokenClaimNames.SUB, subject)); - } - } - int size = this.sessions.size(); - Set infos = new HashSet<>(); - this.sessions.values().removeIf((info) -> { - boolean result = matcher.test(info); - if (result) { - infos.add(info); - } - return result; - }); - if (infos.isEmpty()) { - this.logger.debug("Failed to remove any sessions since none matched"); - } - else if (this.logger.isTraceEnabled()) { - String message = "Found and removed %d session(s) from mapping of %d session(s)"; - this.logger.trace(String.format(message, infos.size(), size)); - } - return infos; - } - - private static Predicate sessionIdMatcher(List audience, String issuer, - String sessionId) { - return (session) -> { - List thatAudience = session.getPrincipal().getAudience(); - String thatIssuer = session.getPrincipal().getIssuer().toString(); - String thatSessionId = session.getPrincipal().getClaimAsString(LogoutTokenClaimNames.SID); - if (thatAudience == null) { - return false; - } - return !Collections.disjoint(audience, thatAudience) && issuer.equals(thatIssuer) - && sessionId.equals(thatSessionId); - }; - } - - private static Predicate subjectMatcher(List audience, String issuer, - String subject) { - return (session) -> { - List thatAudience = session.getPrincipal().getAudience(); - String thatIssuer = session.getPrincipal().getIssuer().toString(); - String thatSubject = session.getPrincipal().getSubject(); - if (thatAudience == null) { - return false; - } - return !Collections.disjoint(audience, thatAudience) && issuer.equals(thatIssuer) - && subject.equals(thatSubject); - }; - } - -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/OidcSessionInformation.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/OidcSessionInformation.java deleted file mode 100644 index d746315178..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/OidcSessionInformation.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2002-2023 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.oidc.session; - -import java.util.Collections; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.security.core.session.SessionInformation; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; - -/** - * A {@link SessionInformation} extension that enforces the principal be of type - * {@link OidcUser}. - * - * @author Josh Cummings - * @since 6.2 - */ -public class OidcSessionInformation extends SessionInformation { - - private final Map authorities; - - /** - * Construct an {@link OidcSessionInformation} - * @param sessionId the Client's session id - * @param authorities any material that authorizes operating on the session - * @param user the OIDC Provider's session and end user - */ - public OidcSessionInformation(String sessionId, Map authorities, OidcUser user) { - super(user, sessionId, new Date()); - this.authorities = (authorities != null) ? new LinkedHashMap<>(authorities) : Collections.emptyMap(); - } - - /** - * Any material needed to authorize operations on this session - * @return the {@link Map} of credentials - */ - public Map getAuthorities() { - return this.authorities; - } - - /** - * {@inheritDoc} - */ - @Override - public OidcUser getPrincipal() { - return (OidcUser) super.getPrincipal(); - } - - /** - * Copy this {@link OidcSessionInformation}, using a new session identifier - * @param sessionId the new session identifier to use - * @return a new {@link OidcSessionInformation} instance - */ - public OidcSessionInformation withSessionId(String sessionId) { - return new OidcSessionInformation(sessionId, getAuthorities(), getPrincipal()); - } - -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/OidcSessionRegistry.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/OidcSessionRegistry.java deleted file mode 100644 index 26bae499db..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/OidcSessionRegistry.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2002-2023 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.oidc.session; - -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; - -/** - * A registry to record the tie between the OIDC Provider session and the Client session. - * This is handy when a provider makes a logout request that indicates the OIDC Provider - * session or the End User. - * - * @author Josh Cummings - * @since 6.2 - * @see Logout - * Token - */ -public interface OidcSessionRegistry { - - /** - * Register a OIDC Provider session with the provided client session. Generally - * speaking, the client session should be the session tied to the current login. - * @param info the {@link OidcSessionInformation} to use - */ - void saveSessionInformation(OidcSessionInformation info); - - /** - * Deregister the OIDC Provider session tied to the provided client session. Generally - * speaking, the client session should be the session tied to the current logout. - * @param clientSessionId the client session - * @return any found {@link OidcSessionInformation}, could be {@code null} - */ - OidcSessionInformation removeSessionInformation(String clientSessionId); - - /** - * Deregister the OIDC Provider sessions referenced by the provided OIDC Logout Token - * by its session id or its subject. Note that the issuer and audience should also - * match the corresponding values found in each {@link OidcSessionInformation} - * returned. - * @param logoutToken the {@link OidcLogoutToken} - * @return any found {@link OidcSessionInformation}s, could be empty - */ - Iterable removeSessionInformation(OidcLogoutToken logoutToken); - -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java index 1cd71aa072..e8e6362479 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java @@ -45,7 +45,7 @@ final class OidcUserRequestUtils { static boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) { // Auto-disabled if UserInfo Endpoint URI is not provided ClientRegistration clientRegistration = userRequest.getClientRegistration(); - if (!StringUtils.hasLength(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri())) { + if (StringUtils.isEmpty(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri())) { return false; } // The Claims requested by the profile, email, address, and phone scope values diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java index 0ae4727ff7..abf3632964 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java @@ -157,7 +157,7 @@ public class OidcUserService implements OAuth2UserServiceRP-Initiated Logout * @see org.springframework.security.web.authentication.logout.LogoutSuccessHandler */ -public class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { +public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { private final ClientRegistrationRepository clientRegistrationRepository; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/SupplierClientRegistrationRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/SupplierClientRegistrationRepository.java deleted file mode 100644 index d044eb33a2..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/SupplierClientRegistrationRepository.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2002-2023 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.registration; - -import java.util.Iterator; -import java.util.function.Supplier; - -import org.springframework.util.Assert; -import org.springframework.util.function.SingletonSupplier; - -/** - * A {@link ClientRegistrationRepository} that lazily calls to retrieve - * {@link ClientRegistration}(s) when requested. - * - * @author Justin Tay - * @since 6.2 - * @see ClientRegistrationRepository - * @see ClientRegistration - */ -public final class SupplierClientRegistrationRepository - implements ClientRegistrationRepository, Iterable { - - private final Supplier repositorySupplier; - - /** - * Constructs an {@code SupplierClientRegistrationRepository} using the provided - * parameters. - * @param repositorySupplier the client registration repository supplier - */ - public > SupplierClientRegistrationRepository( - Supplier repositorySupplier) { - Assert.notNull(repositorySupplier, "repositorySupplier cannot be null"); - this.repositorySupplier = SingletonSupplier.of(repositorySupplier); - } - - @Override - public ClientRegistration findByRegistrationId(String registrationId) { - Assert.hasText(registrationId, "registrationId cannot be empty"); - return this.repositorySupplier.get().findByRegistrationId(registrationId); - } - - /** - * Returns an {@code Iterator} of {@link ClientRegistration}. - * @return an {@code Iterator} - */ - @Override - public Iterator iterator() { - return ((Iterable) this.repositorySupplier.get()).iterator(); - } - -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java index 90c33fd41b..f24461e3fe 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java @@ -195,7 +195,7 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi private static Mono parse(ClientResponse httpResponse) { String wwwAuth = httpResponse.headers().asHttpHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE); - if (StringUtils.hasLength(wwwAuth)) { + if (!StringUtils.isEmpty(wwwAuth)) { // Bearer token error? return Mono.fromCallable(() -> UserInfoErrorResponse.parse(wwwAuth)); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java index 9c04e1d56d..69a9987669 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java @@ -238,10 +238,10 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter { OAuth2Error error = ex.getError(); UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri()) .queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode()); - if (StringUtils.hasLength(error.getDescription())) { + if (!StringUtils.isEmpty(error.getDescription())) { uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription()); } - if (StringUtils.hasLength(error.getUri())) { + if (!StringUtils.isEmpty(error.getUri())) { uriBuilder.queryParam(OAuth2ParameterNames.ERROR_URI, error.getUri()); } this.redirectStrategy.sendRedirect(request, response, uriBuilder.build().encode().toString()); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java index 4ac3d7d7ea..6524d9230d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java @@ -111,7 +111,7 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) { String clientRegistrationId = this.resolveClientRegistrationId(parameter); - if (!StringUtils.hasLength(clientRegistrationId)) { + if (StringUtils.isEmpty(clientRegistrationId)) { throw new IllegalArgumentException("Unable to resolve the Client Registration Identifier. " + "It must be provided via @RegisteredOAuth2AuthorizedClient(\"client1\") or " + "@RegisteredOAuth2AuthorizedClient(registrationId = \"client1\")."); @@ -137,10 +137,10 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth RegisteredOAuth2AuthorizedClient authorizedClientAnnotation = AnnotatedElementUtils .findMergedAnnotation(parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class); Authentication principal = this.securityContextHolderStrategy.getContext().getAuthentication(); - if (StringUtils.hasLength(authorizedClientAnnotation.registrationId())) { + if (!StringUtils.isEmpty(authorizedClientAnnotation.registrationId())) { return authorizedClientAnnotation.registrationId(); } - if (StringUtils.hasLength(authorizedClientAnnotation.value())) { + if (!StringUtils.isEmpty(authorizedClientAnnotation.value())) { return authorizedClientAnnotation.value(); } if (principal != null && OAuth2AuthenticationToken.class.isAssignableFrom(principal.getClass())) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java index ffdb93d165..136cf6ebe6 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java @@ -524,7 +524,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements authParameters.get(OAuth2ParameterNames.ERROR_URI)); } } - return resolveErrorIfPossible(response.statusCode().value()); + return resolveErrorIfPossible(response.rawStatusCode()); } private OAuth2Error resolveErrorIfPossible(int statusCode) { @@ -538,7 +538,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements private Map parseAuthParameters(String wwwAuthenticateHeader) { // @formatter:off return Stream.of(wwwAuthenticateHeader) - .filter((header) -> StringUtils.hasLength(header)) + .filter((header) -> !StringUtils.isEmpty(header)) .filter((header) -> header.toLowerCase().startsWith("bearer")) .map((header) -> header.substring("bearer".length())) .map((header) -> header.split(",")) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java index d03f648d04..b2eb584051 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java @@ -640,7 +640,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement authParameters.get(OAuth2ParameterNames.ERROR_URI)); } } - return resolveErrorIfPossible(response.statusCode().value()); + return resolveErrorIfPossible(response.rawStatusCode()); } private OAuth2Error resolveErrorIfPossible(int statusCode) { @@ -653,7 +653,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement private Map parseAuthParameters(String wwwAuthenticateHeader) { // @formatter:off - return Stream.of(wwwAuthenticateHeader).filter((header) -> StringUtils.hasLength(header)) + return Stream.of(wwwAuthenticateHeader).filter((header) -> !StringUtils.isEmpty(header)) .filter((header) -> header.toLowerCase().startsWith("bearer")) .map((header) -> header.substring("bearer".length())) .map((header) -> header.split(",")) diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManagerTests.java index e250ebbe78..af856a2396 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManagerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManagerTests.java @@ -70,13 +70,13 @@ public class OAuth2AuthorizationCodeReactiveAuthenticationManagerTests { @Test public void authenticateWhenErrorThenOAuth2AuthorizationException() { this.authorizationResponse = TestOAuth2AuthorizationResponses.error(); - assertThatExceptionOfType(OAuth2AuthorizationException.class).isThrownBy(this::authenticate); + assertThatExceptionOfType(OAuth2AuthorizationException.class).isThrownBy(() -> authenticate()); } @Test public void authenticateWhenStateNotEqualThenOAuth2AuthorizationException() { this.authorizationRequest.state("notequal"); - assertThatExceptionOfType(OAuth2AuthorizationException.class).isThrownBy(this::authenticate); + assertThatExceptionOfType(OAuth2AuthorizationException.class).isThrownBy(() -> authenticate()); } @Test diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClientTests.java index 4aed182e02..d5e4cd9949 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClientTests.java @@ -130,7 +130,7 @@ public class DefaultAuthorizationCodeTokenResponseClientTests { assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBetween(expiresAtBefore, expiresAtAfter); assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write"); assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo("refresh-token-1234"); - assertThat(accessTokenResponse.getAdditionalParameters()).hasSize(2); + assertThat(accessTokenResponse.getAdditionalParameters().size()).isEqualTo(2); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_1", "custom-value-1"); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_2", "custom-value-2"); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java index cc8a62f8aa..89431381d5 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java @@ -132,7 +132,7 @@ public class DefaultClientCredentialsTokenResponseClientTests { assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBetween(expiresAtBefore, expiresAtAfter); assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write"); assertThat(accessTokenResponse.getRefreshToken()).isNull(); - assertThat(accessTokenResponse.getAdditionalParameters()).hasSize(2); + assertThat(accessTokenResponse.getAdditionalParameters().size()).isEqualTo(2); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_1", "custom-value-1"); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_2", "custom-value-2"); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverterTests.java index f4cd199170..6fd2833cd5 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverterTests.java @@ -140,8 +140,8 @@ public class NimbusJwtClientAuthenticationParametersConverterTests { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(rsaJwk.toRSAPublicKey()).build(); Jwt jws = jwtDecoder.decode(encodedJws); - assertThat(jws.getHeaders()).containsEntry(JoseHeaderNames.ALG, SignatureAlgorithm.RS256.getName()); - assertThat(jws.getHeaders()).containsEntry(JoseHeaderNames.KID, rsaJwk.getKeyID()); + assertThat(jws.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(SignatureAlgorithm.RS256.getName()); + assertThat(jws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(rsaJwk.getKeyID()); assertThat(jws.getClaim(JwtClaimNames.ISS)).isEqualTo(clientRegistration.getClientId()); assertThat(jws.getSubject()).isEqualTo(clientRegistration.getClientId()); assertThat(jws.getAudience()) @@ -174,8 +174,8 @@ public class NimbusJwtClientAuthenticationParametersConverterTests { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(secretJwk.toSecretKey()).build(); Jwt jws = jwtDecoder.decode(encodedJws); - assertThat(jws.getHeaders()).containsEntry(JoseHeaderNames.ALG, MacAlgorithm.HS256.getName()); - assertThat(jws.getHeaders()).containsEntry(JoseHeaderNames.KID, secretJwk.getKeyID()); + assertThat(jws.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(MacAlgorithm.HS256.getName()); + assertThat(jws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(secretJwk.getKeyID()); assertThat(jws.getClaim(JwtClaimNames.ISS)).isEqualTo(clientRegistration.getClientId()); assertThat(jws.getSubject()).isEqualTo(clientRegistration.getClientId()); assertThat(jws.getAudience()) @@ -217,9 +217,9 @@ public class NimbusJwtClientAuthenticationParametersConverterTests { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(secretJwk.toSecretKey()).build(); Jwt jws = jwtDecoder.decode(encodedJws); - assertThat(jws.getHeaders()).containsEntry(JoseHeaderNames.ALG, MacAlgorithm.HS256.getName()); - assertThat(jws.getHeaders()).containsEntry(JoseHeaderNames.KID, secretJwk.getKeyID()); - assertThat(jws.getHeaders()).containsEntry(headerName, headerValue); + assertThat(jws.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(MacAlgorithm.HS256.getName()); + assertThat(jws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(secretJwk.getKeyID()); + assertThat(jws.getHeaders().get(headerName)).isEqualTo(headerValue); assertThat(jws.getClaim(JwtClaimNames.ISS)).isEqualTo(clientRegistration.getClientId()); assertThat(jws.getSubject()).isEqualTo(clientRegistration.getClientId()); assertThat(jws.getAudience()) diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java index cd7c1d31f5..1da58cab87 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java @@ -118,7 +118,7 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClientTests { assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBetween(expiresAtBefore, expiresAtAfter); assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("openid", "profile"); assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo("refresh-token-1234"); - assertThat(accessTokenResponse.getAdditionalParameters()).hasSize(2); + assertThat(accessTokenResponse.getAdditionalParameters().size()).isEqualTo(2); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_1", "custom-value-1"); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_2", "custom-value-2"); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClientTests.java index 380240b189..99e8443e0c 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveClientCredentialsTokenResponseClientTests.java @@ -101,7 +101,7 @@ public class WebClientReactiveClientCredentialsTokenResponseClientTests { this.clientRegistration.build()); OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block(); RecordedRequest actualRequest = this.server.takeRequest(); - String body = actualRequest.getBody().readUtf8(); + String body = actualRequest.getUtf8Body(); assertThat(response.getAccessToken()).isNotNull(); assertThat(response.getAccessToken().getScopes()).containsExactly("create"); assertThat(actualRequest.getHeader(HttpHeaders.AUTHORIZATION)) @@ -157,7 +157,7 @@ public class WebClientReactiveClientCredentialsTokenResponseClientTests { OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(registration); OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block(); RecordedRequest actualRequest = this.server.takeRequest(); - String body = actualRequest.getBody().readUtf8(); + String body = actualRequest.getUtf8Body(); assertThat(response.getAccessToken()).isNotNull(); assertThat(response.getAccessToken().getScopes()).containsExactly("create"); assertThat(actualRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull(); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java index e4c76b69db..8d100fddca 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java @@ -229,7 +229,7 @@ public class OAuth2AuthenticationTokenMixinTests { String authoritiesJson = (oidcUserAuthority != null) ? asJson(oidcUserAuthority) : (oauth2UserAuthority != null) ? asJson(oauth2UserAuthority) : ""; if (!simpleAuthorities.isEmpty()) { - if (StringUtils.hasLength(authoritiesJson)) { + if (!StringUtils.isEmpty(authoritiesJson)) { authoritiesJson += ","; } authoritiesJson += asJson(simpleAuthorities); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/logout/TestOidcLogoutTokens.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/logout/TestOidcLogoutTokens.java deleted file mode 100644 index 64602687f6..0000000000 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/logout/TestOidcLogoutTokens.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2002-2023 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.oidc.authentication.logout; - -import java.time.Instant; -import java.util.Collections; - -import org.springframework.security.oauth2.core.oidc.user.OidcUser; - -public final class TestOidcLogoutTokens { - - public static OidcLogoutToken.Builder withUser(OidcUser user) { - OidcLogoutToken.Builder builder = OidcLogoutToken.withTokenValue("token") - .audience(Collections.singleton("client-id")) - .issuedAt(Instant.now()) - .issuer(user.getIssuer().toString()) - .jti("id") - .subject(user.getSubject()); - if (user.hasClaim(LogoutTokenClaimNames.SID)) { - builder.sessionId(user.getClaimAsString(LogoutTokenClaimNames.SID)); - } - return builder; - } - - public static OidcLogoutToken.Builder withSessionId(String issuer, String sessionId) { - return OidcLogoutToken.withTokenValue("token") - .audience(Collections.singleton("client-id")) - .issuedAt(Instant.now()) - .issuer(issuer) - .jti("id") - .sessionId(sessionId); - } - - public static OidcLogoutToken.Builder withSubject(String issuer, String subject) { - return OidcLogoutToken.withTokenValue("token") - .audience(Collections.singleton("client-id")) - .issuedAt(Instant.now()) - .issuer(issuer) - .jti("id") - .subject(subject); - } - - private TestOidcLogoutTokens() { - - } - -} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistryTests.java deleted file mode 100644 index 6064e74b03..0000000000 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistryTests.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2002-2023 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.oidc.session; - -import org.junit.jupiter.api.Test; - -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; -import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens; -import org.springframework.security.oauth2.core.oidc.OidcIdToken; -import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens; -import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link InMemoryOidcSessionRegistry} - */ -public class InMemoryOidcSessionRegistryTests { - - @Test - public void registerWhenDefaultsThenStoresSessionInformation() { - InMemoryOidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); - String sessionId = "client"; - OidcSessionInformation info = TestOidcSessionInformations.create(sessionId); - sessionRegistry.saveSessionInformation(info); - OidcLogoutToken logoutToken = TestOidcLogoutTokens.withUser(info.getPrincipal()).build(); - Iterable infos = sessionRegistry.removeSessionInformation(logoutToken); - assertThat(infos).containsExactly(info); - } - - @Test - public void registerWhenIdTokenHasSessionIdThenStoresSessionInformation() { - InMemoryOidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); - OidcIdToken idToken = TestOidcIdTokens.idToken().claim("sid", "provider").build(); - OidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken); - OidcSessionInformation info = TestOidcSessionInformations.create("client", user); - sessionRegistry.saveSessionInformation(info); - OidcLogoutToken logoutToken = TestOidcLogoutTokens.withSessionId(idToken.getIssuer().toString(), "provider") - .build(); - Iterable infos = sessionRegistry.removeSessionInformation(logoutToken); - assertThat(infos).containsExactly(info); - } - - @Test - public void unregisterWhenMultipleSessionsThenRemovesAllMatching() { - InMemoryOidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); - OidcIdToken idToken = TestOidcIdTokens.idToken().claim("sid", "providerOne").subject("otheruser").build(); - OidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken); - OidcSessionInformation oneSession = TestOidcSessionInformations.create("clientOne", user); - sessionRegistry.saveSessionInformation(oneSession); - idToken = TestOidcIdTokens.idToken().claim("sid", "providerTwo").build(); - user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken); - OidcSessionInformation twoSession = TestOidcSessionInformations.create("clientTwo", user); - sessionRegistry.saveSessionInformation(twoSession); - idToken = TestOidcIdTokens.idToken().claim("sid", "providerThree").build(); - user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken); - OidcSessionInformation threeSession = TestOidcSessionInformations.create("clientThree", user); - sessionRegistry.saveSessionInformation(threeSession); - OidcLogoutToken logoutToken = TestOidcLogoutTokens - .withSubject(idToken.getIssuer().toString(), idToken.getSubject()) - .build(); - Iterable infos = sessionRegistry.removeSessionInformation(logoutToken); - assertThat(infos).containsExactlyInAnyOrder(twoSession, threeSession); - logoutToken = TestOidcLogoutTokens.withSubject(idToken.getIssuer().toString(), "otheruser").build(); - infos = sessionRegistry.removeSessionInformation(logoutToken); - assertThat(infos).containsExactly(oneSession); - } - - @Test - public void unregisterWhenNoSessionsThenEmptyList() { - InMemoryOidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry(); - OidcIdToken idToken = TestOidcIdTokens.idToken().claim("sid", "provider").build(); - OidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken); - OidcSessionInformation info = TestOidcSessionInformations.create("client", user); - sessionRegistry.saveSessionInformation(info); - OidcLogoutToken logoutToken = TestOidcLogoutTokens.withSessionId(idToken.getIssuer().toString(), "wrong") - .build(); - Iterable infos = sessionRegistry.removeSessionInformation(logoutToken); - assertThat(infos).isNotNull(); - assertThat(infos).isEmpty(); - logoutToken = TestOidcLogoutTokens.withSessionId("https://wrong", "provider").build(); - infos = sessionRegistry.removeSessionInformation(logoutToken); - assertThat(infos).isNotNull(); - assertThat(infos).isEmpty(); - } - -} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/TestOidcSessionInformations.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/TestOidcSessionInformations.java deleted file mode 100644 index 47f64868de..0000000000 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/TestOidcSessionInformations.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2002-2023 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.oidc.session; - -import java.util.Map; - -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers; - -/** - * Sample {@link OidcSessionInformation} instances - */ -public final class TestOidcSessionInformations { - - public static OidcSessionInformation create() { - return create("sessionId"); - } - - public static OidcSessionInformation create(String sessionId) { - return create(sessionId, TestOidcUsers.create()); - } - - public static OidcSessionInformation create(String sessionId, OidcUser user) { - return new OidcSessionInformation(sessionId, Map.of("_csrf", "token"), user); - } - - private TestOidcSessionInformations() { - - } - -} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java index 310667e2ff..45ca6f34b1 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java @@ -235,7 +235,7 @@ public class OidcUserServiceTests { .loadUser(new OidcUserRequest(clientRegistration, this.accessToken, this.idToken)); assertThat(user.getIdToken()).isNotNull(); assertThat(user.getUserInfo()).isNotNull(); - assertThat(user.getUserInfo().getClaims()).hasSize(6); + assertThat(user.getUserInfo().getClaims().size()).isEqualTo(6); assertThat(user.getIdToken()).isEqualTo(this.idToken); assertThat(user.getName()).isEqualTo("subject1"); assertThat(user.getUserInfo().getSubject()).isEqualTo("subject1"); @@ -244,7 +244,7 @@ public class OidcUserServiceTests { assertThat(user.getUserInfo().getFamilyName()).isEqualTo("last"); assertThat(user.getUserInfo().getPreferredUsername()).isEqualTo("user1"); assertThat(user.getUserInfo().getEmail()).isEqualTo("user1@example.com"); - assertThat(user.getAuthorities()).hasSize(3); + assertThat(user.getAuthorities().size()).isEqualTo(3); assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OidcUserAuthority.class); OidcUserAuthority userAuthority = (OidcUserAuthority) user.getAuthorities().iterator().next(); assertThat(userAuthority.getAuthority()).isEqualTo("OIDC_USER"); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java index b3b74e805d..2da662c82e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java @@ -479,12 +479,12 @@ public class ClientRegistrationsTests { final Dispatcher dispatcher = new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) { - return switch (request.getPath()) { - case "/.well-known/oauth-authorization-server/issuer1", - "/.well-known/oauth-authorization-server/" -> - buildSuccessMockResponse(responseBody); - default -> new MockResponse().setResponseCode(404); - }; + switch (request.getPath()) { + case "/.well-known/oauth-authorization-server/issuer1": + case "/.well-known/oauth-authorization-server/": + return buildSuccessMockResponse(responseBody); + } + return new MockResponse().setResponseCode(404); } }; this.server.setDispatcher(dispatcher); @@ -515,11 +515,12 @@ public class ClientRegistrationsTests { final Dispatcher dispatcher = new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) { - return switch (request.getPath()) { - case "/issuer1/.well-known/openid-configuration", "/.well-known/openid-configuration/" -> - buildSuccessMockResponse(responseBody); - default -> new MockResponse().setResponseCode(404); - }; + switch (request.getPath()) { + case "/issuer1/.well-known/openid-configuration": + case "/.well-known/openid-configuration/": + return buildSuccessMockResponse(responseBody); + } + return new MockResponse().setResponseCode(404); } }; this.server.setDispatcher(dispatcher); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/SupplierClientRegistrationRepositoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/SupplierClientRegistrationRepositoryTests.java deleted file mode 100644 index e7ae981d87..0000000000 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/SupplierClientRegistrationRepositoryTests.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2002-2023 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.registration; - -import java.util.Collections; -import java.util.function.Supplier; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link SupplierClientRegistrationRepository}. - * - * @author Justin Tay - * @since 6.2 - */ -@ExtendWith(MockitoExtension.class) -public class SupplierClientRegistrationRepositoryTests { - - private ClientRegistration registration = TestClientRegistrations.clientRegistration().build(); - - private SupplierClientRegistrationRepository clients = new SupplierClientRegistrationRepository( - () -> new InMemoryClientRegistrationRepository(this.registration)); - - @Mock - Supplier clientRegistrationRepositorySupplier; - - @Test - public void constructorMapClientRegistrationWhenNullThenIllegalArgumentException() { - assertThatIllegalArgumentException().isThrownBy(() -> new SupplierClientRegistrationRepository(null)); - } - - @Test - public void constructorMapClientRegistrationWhenEmptyMapThenRepositoryIsEmpty() { - SupplierClientRegistrationRepository clients = new SupplierClientRegistrationRepository( - () -> new InMemoryClientRegistrationRepository(Collections.emptyMap())); - assertThat(clients).isEmpty(); - } - - @Test - public void findByRegistrationIdWhenFoundThenFound() { - String id = this.registration.getRegistrationId(); - assertThat(this.clients.findByRegistrationId(id)).isEqualTo(this.registration); - } - - @Test - public void findByRegistrationIdWhenNotFoundThenNull() { - String id = this.registration.getRegistrationId() + "MISSING"; - assertThat(this.clients.findByRegistrationId(id)).isNull(); - } - - @Test - public void findByRegistrationIdWhenNullIdThenIllegalArgumentException() { - assertThatIllegalArgumentException().isThrownBy(() -> this.clients.findByRegistrationId(null)); - } - - @Test - public void findByRegistrationIdThenSingletonSupplierCached() { - SupplierClientRegistrationRepository test = new SupplierClientRegistrationRepository( - this.clientRegistrationRepositorySupplier); - given(this.clientRegistrationRepositorySupplier.get()) - .willReturn(new InMemoryClientRegistrationRepository(this.registration)); - String id = this.registration.getRegistrationId(); - assertThat(test.findByRegistrationId(id)).isEqualTo(this.registration); - id = this.registration.getRegistrationId(); - assertThat(test.findByRegistrationId(id)).isEqualTo(this.registration); - verify(this.clientRegistrationRepositorySupplier, times(1)).get(); - } - - @Test - public void iteratorWhenRemoveThenThrowsUnsupportedOperationException() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(this.clients.iterator()::remove); - } - - @Test - public void iteratorWhenGetThenContainsAll() { - assertThat(this.clients).containsOnly(this.registration); - } - -} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java index 361100ec6f..871d20effe 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java @@ -144,14 +144,14 @@ public class DefaultOAuth2UserServiceTests { .build(); OAuth2User user = this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)); assertThat(user.getName()).isEqualTo("user1"); - assertThat(user.getAttributes()).hasSize(6); + assertThat(user.getAttributes().size()).isEqualTo(6); assertThat((String) user.getAttribute("user-name")).isEqualTo("user1"); assertThat((String) user.getAttribute("first-name")).isEqualTo("first"); assertThat((String) user.getAttribute("last-name")).isEqualTo("last"); assertThat((String) user.getAttribute("middle-name")).isEqualTo("middle"); assertThat((String) user.getAttribute("address")).isEqualTo("address"); assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com"); - assertThat(user.getAuthorities()).hasSize(1); + assertThat(user.getAuthorities().size()).isEqualTo(1); assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class); OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next(); assertThat(userAuthority.getAuthority()).isEqualTo("OAUTH2_USER"); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java index c9989ae320..039b09205e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java @@ -132,14 +132,14 @@ public class DefaultReactiveOAuth2UserServiceTests { enqueueApplicationJsonBody(userInfoResponse); OAuth2User user = this.userService.loadUser(oauth2UserRequest()).block(); assertThat(user.getName()).isEqualTo("user1"); - assertThat(user.getAttributes()).hasSize(6); + assertThat(user.getAttributes().size()).isEqualTo(6); assertThat((String) user.getAttribute("id")).isEqualTo("user1"); assertThat((String) user.getAttribute("first-name")).isEqualTo("first"); assertThat((String) user.getAttribute("last-name")).isEqualTo("last"); assertThat((String) user.getAttribute("middle-name")).isEqualTo("middle"); assertThat((String) user.getAttribute("address")).isEqualTo("address"); assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com"); - assertThat(user.getAuthorities()).hasSize(1); + assertThat(user.getAuthorities().size()).isEqualTo(1); assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class); OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next(); assertThat(userAuthority.getAuthority()).isEqualTo("OAUTH2_USER"); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java index f847d2b5ec..eb83df5b27 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -495,7 +495,7 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests { .attributes(ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(authorizedClient)) .build(); // @formatter:on - given(this.exchange.getResponse().statusCode()).willReturn(HttpStatus.UNAUTHORIZED); + given(this.exchange.getResponse().rawStatusCode()).willReturn(HttpStatus.UNAUTHORIZED.value()); this.function.filter(request, this.exchange).contextWrite(serverWebExchange()).block(); assertThat(publisherProbe.wasSubscribed()).isTrue(); verify(this.authorizationFailureHandler).onAuthorizationFailure(this.authorizationExceptionCaptor.capture(), @@ -570,7 +570,7 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests { .attributes(ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(authorizedClient)) .build(); // @formatter:on - given(this.exchange.getResponse().statusCode()).willReturn(HttpStatus.FORBIDDEN); + given(this.exchange.getResponse().rawStatusCode()).willReturn(HttpStatus.FORBIDDEN.value()); this.function.filter(request, this.exchange).contextWrite(serverWebExchange()).block(); assertThat(publisherProbe.wasSubscribed()).isTrue(); verify(this.authorizationFailureHandler).onAuthorizationFailure(this.authorizationExceptionCaptor.capture(), @@ -701,7 +701,7 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests { ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("https://example.com")) .attributes(ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(authorizedClient)) .build(); - given(this.exchange.getResponse().statusCode()).willReturn(HttpStatus.BAD_REQUEST); + given(this.exchange.getResponse().rawStatusCode()).willReturn(HttpStatus.BAD_REQUEST.value()); this.function.filter(request, this.exchange).contextWrite(serverWebExchange()).block(); verify(this.authorizationFailureHandler, never()).onAuthorizationFailure(any(), any(), any()); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java index c963a11897..872781be59 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -680,7 +680,7 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests { .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.httpServletRequest(servletRequest)) .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.httpServletResponse(servletResponse)) .build(); - given(this.exchange.getResponse().statusCode()).willReturn(httpStatus); + given(this.exchange.getResponse().rawStatusCode()).willReturn(httpStatus.value()); given(this.exchange.getResponse().headers()).willReturn(mock(ClientResponse.Headers.class)); this.function.setAuthorizationFailureHandler(this.authorizationFailureHandler); this.function.filter(request, this.exchange).block(); @@ -825,7 +825,7 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests { .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.httpServletRequest(servletRequest)) .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.httpServletResponse(servletResponse)) .build(); - given(this.exchange.getResponse().statusCode()).willReturn(HttpStatus.BAD_REQUEST); + given(this.exchange.getResponse().rawStatusCode()).willReturn(HttpStatus.BAD_REQUEST.value()); given(this.exchange.getResponse().headers()).willReturn(mock(ClientResponse.Headers.class)); this.function.setAuthorizationFailureHandler(this.authorizationFailureHandler); this.function.filter(request, this.exchange).block(); diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java index 07b16e2b74..29501ae514 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,6 @@ import org.springframework.util.Assert; * extensibility mechanism for defining additional grant types. * * @author Joe Grandja - * @author Steve Riesenberg * @since 5.0 * @see Section * 1.3 Authorization Grant @@ -63,12 +62,6 @@ public final class AuthorizationGrantType implements Serializable { public static final AuthorizationGrantType JWT_BEARER = new AuthorizationGrantType( "urn:ietf:params:oauth:grant-type:jwt-bearer"); - /** - * @since 6.1 - */ - public static final AuthorizationGrantType DEVICE_CODE = new AuthorizationGrantType( - "urn:ietf:params:oauth:grant-type:device_code"); - private final String value; /** diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java index 46256b58d3..09e253ebb3 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -107,7 +107,7 @@ public interface ClaimAccessor { } Object claimValue = getClaims().get(claim); Instant convertedValue = ClaimConversionService.getSharedInstance().convert(claimValue, Instant.class); - Assert.notNull(convertedValue, + Assert.isTrue(convertedValue != null, () -> "Unable to convert claim '" + claim + "' of type '" + claimValue.getClass() + "' to Instant."); return convertedValue; } @@ -123,7 +123,7 @@ public interface ClaimAccessor { } Object claimValue = getClaims().get(claim); URL convertedValue = ClaimConversionService.getSharedInstance().convert(claimValue, URL.class); - Assert.notNull(convertedValue, + Assert.isTrue(convertedValue != null, () -> "Unable to convert claim '" + claim + "' of type '" + claimValue.getClass() + "' to URL."); return convertedValue; } @@ -148,7 +148,7 @@ public interface ClaimAccessor { Object claimValue = getClaims().get(claim); Map convertedValue = (Map) ClaimConversionService.getSharedInstance() .convert(claimValue, sourceDescriptor, targetDescriptor); - Assert.notNull(convertedValue, + Assert.isTrue(convertedValue != null, () -> "Unable to convert claim '" + claim + "' of type '" + claimValue.getClass() + "' to Map."); return convertedValue; } @@ -173,7 +173,7 @@ public interface ClaimAccessor { Object claimValue = getClaims().get(claim); List convertedValue = (List) ClaimConversionService.getSharedInstance() .convert(claimValue, sourceDescriptor, targetDescriptor); - Assert.notNull(convertedValue, + Assert.isTrue(convertedValue != null, () -> "Unable to convert claim '" + claim + "' of type '" + claimValue.getClass() + "' to List."); return convertedValue; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java index 0b986dcc55..fdb843a1ab 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java @@ -98,9 +98,4 @@ public final class ClientAuthenticationMethod implements Serializable { return getValue().hashCode(); } - @Override - public String toString() { - return this.value; - } - } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2DeviceCode.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2DeviceCode.java deleted file mode 100644 index c2127afdda..0000000000 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2DeviceCode.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.core; - -import java.time.Instant; - -/** - * An implementation of an {@link AbstractOAuth2Token} representing a device code as part - * of the OAuth 2.0 Device Authorization Grant. - * - * @author Steve Riesenberg - * @since 6.1 - * @see OAuth2UserCode - * @see Section - * 3.2 Device Authorization Response - */ -public class OAuth2DeviceCode extends AbstractOAuth2Token { - - /** - * Constructs an {@code OAuth2DeviceCode} using the provided parameters. - * @param tokenValue the token value - * @param issuedAt the time at which the token was issued - * @param expiresAt the time at which the token expires - */ - public OAuth2DeviceCode(String tokenValue, Instant issuedAt, Instant expiresAt) { - super(tokenValue, issuedAt, expiresAt); - } - -} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2UserCode.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2UserCode.java deleted file mode 100644 index 31d6b6b609..0000000000 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2UserCode.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.core; - -import java.time.Instant; - -/** - * An implementation of an {@link AbstractOAuth2Token} representing a user code as part of - * the OAuth 2.0 Device Authorization Grant. - * - * @author Steve Riesenberg - * @since 6.1 - * @see OAuth2DeviceCode - * @see Section - * 3.2 Device Authorization Response - */ -public class OAuth2UserCode extends AbstractOAuth2Token { - - /** - * Constructs an {@code OAuth2UserCode} using the provided parameters. - * @param tokenValue the token value - * @param issuedAt the time at which the token was issued - * @param expiresAt the time at which the token expires - */ - public OAuth2UserCode(String tokenValue, Instant issuedAt, Instant expiresAt) { - super(tokenValue, issuedAt, expiresAt); - } - -} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagers.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagers.java deleted file mode 100644 index 7697b4a9d6..0000000000 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagers.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.core.authorization; - -import org.springframework.security.authorization.AuthorityAuthorizationManager; -import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.core.Authentication; -import org.springframework.util.Assert; - -/** - * A convenience class for creating OAuth 2.0-specific {@link AuthorizationManager}s. - * - * @author Mario Petrovski - * @author Josh Cummings - * @since 6.2 - * @see AuthorityAuthorizationManager - */ -public final class OAuth2AuthorizationManagers { - - private OAuth2AuthorizationManagers() { - } - - /** - * Create an {@link AuthorizationManager} that requires an {@link Authentication} to - * have a {@code SCOPE_scope} authority. - * - *

      - * For example, if you call {@code hasScope("read")}, then this will require that each - * authentication have a {@link org.springframework.security.core.GrantedAuthority} - * whose value is {@code SCOPE_read}. - * - *

      - * This would equivalent to calling - * {@code AuthorityAuthorizationManager#hasAuthority("SCOPE_read")}. - * @param scope the scope value to require - * @param the secure object - * @return an {@link AuthorizationManager} that requires a {@code "SCOPE_scope"} - * authority - */ - public static AuthorizationManager hasScope(String scope) { - assertScope(scope); - return AuthorityAuthorizationManager.hasAuthority("SCOPE_" + scope); - } - - /** - * Create an {@link AuthorizationManager} that requires an {@link Authentication} to - * have at least one authority among {@code SCOPE_scope1}, {@code SCOPE_scope2}, ... - * {@code SCOPE_scopeN}. - * - *

      - * For example, if you call {@code hasAnyScope("read", "write")}, then this will - * require that each authentication have at least a - * {@link org.springframework.security.core.GrantedAuthority} whose value is either - * {@code SCOPE_read} or {@code SCOPE_write}. - * - *

      - * This would equivalent to calling - * {@code AuthorityAuthorizationManager#hasAnyAuthority("SCOPE_read", "SCOPE_write")}. - * @param scopes the scope values to allow - * @param the secure object - * @return an {@link AuthorizationManager} that requires at least one authority among - * {@code "SCOPE_scope1"}, {@code SCOPE_scope2}, ... {@code SCOPE_scopeN}. - * - */ - public static AuthorizationManager hasAnyScope(String... scopes) { - String[] mappedScopes = new String[scopes.length]; - for (int i = 0; i < scopes.length; i++) { - assertScope(scopes[i]); - mappedScopes[i] = "SCOPE_" + scopes[i]; - } - return AuthorityAuthorizationManager.hasAnyAuthority(mappedScopes); - } - - private static void assertScope(String scope) { - Assert.isTrue(!scope.startsWith("SCOPE_"), - () -> scope + " should not start with SCOPE_ since SCOPE_" - + " is automatically prepended when using hasScope and hasAnyScope. Consider using " - + " AuthorityAuthorizationManager#hasAuthority or #hasAnyAuthority instead."); - } - -} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2ReactiveAuthorizationManagers.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2ReactiveAuthorizationManagers.java deleted file mode 100644 index 753cf1aabd..0000000000 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/OAuth2ReactiveAuthorizationManagers.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.core.authorization; - -import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager; -import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.authorization.ReactiveAuthorizationManager; -import org.springframework.security.core.Authentication; -import org.springframework.util.Assert; - -/** - * A convenience class for creating OAuth 2.0-specific {@link AuthorizationManager}s. - * - * @author Josh Cummings - * @since 6.2 - * @see AuthorityReactiveAuthorizationManager - */ -public final class OAuth2ReactiveAuthorizationManagers { - - private OAuth2ReactiveAuthorizationManagers() { - } - - /** - * Create a {@link ReactiveAuthorizationManager} that requires an - * {@link Authentication} to have a {@code SCOPE_scope} authority. - * - *

      - * For example, if you call {@code hasScope("read")}, then this will require that each - * authentication have a {@link org.springframework.security.core.GrantedAuthority} - * whose value is {@code SCOPE_read}. - * - *

      - * This would equivalent to calling - * {@code AuthorityReactiveAuthorizationManager#hasAuthority("SCOPE_read")}. - * @param scope the scope value to require - * @param the secure object - * @return an {@link ReactiveAuthorizationManager} that requires a - * {@code "SCOPE_scope"} authority - */ - public static ReactiveAuthorizationManager hasScope(String scope) { - assertScope(scope); - return AuthorityReactiveAuthorizationManager.hasAuthority("SCOPE_" + scope); - } - - /** - * Create a {@link ReactiveAuthorizationManager} that requires an - * {@link Authentication} to have at least one authority among {@code SCOPE_scope1}, - * {@code SCOPE_scope2}, ... {@code SCOPE_scopeN}. - * - *

      - * For example, if you call {@code hasAnyScope("read", "write")}, then this will - * require that each authentication have at least a - * {@link org.springframework.security.core.GrantedAuthority} whose value is either - * {@code SCOPE_read} or {@code SCOPE_write}. - * - *

      - * This would equivalent to calling - * {@code AuthorityReactiveAuthorizationManager#hasAnyAuthority("SCOPE_read", "SCOPE_write")}. - * @param scopes the scope values to allow - * @param the secure object - * @return an {@link ReactiveAuthorizationManager} that requires at least one - * authority among {@code "SCOPE_scope1"}, {@code SCOPE_scope2}, ... - * {@code SCOPE_scopeN}. - */ - public static ReactiveAuthorizationManager hasAnyScope(String... scopes) { - String[] mappedScopes = new String[scopes.length]; - for (int i = 0; i < scopes.length; i++) { - assertScope(scopes[i]); - mappedScopes[i] = "SCOPE_" + scopes[i]; - } - return AuthorityReactiveAuthorizationManager.hasAnyAuthority(mappedScopes); - } - - private static void assertScope(String scope) { - Assert.isTrue(!scope.startsWith("SCOPE_"), - () -> scope + " should not start with SCOPE_ since SCOPE_" - + " is automatically prepended when using hasScope and hasAnyScope. Consider using " - + " AuthorityReactiveAuthorizationManager#hasAuthority or #hasAnyAuthority instead."); - } - -} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2DeviceAuthorizationResponse.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2DeviceAuthorizationResponse.java deleted file mode 100644 index a832d875eb..0000000000 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2DeviceAuthorizationResponse.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.core.endpoint; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Collections; -import java.util.Map; - -import org.springframework.security.oauth2.core.OAuth2DeviceCode; -import org.springframework.security.oauth2.core.OAuth2UserCode; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; - -/** - * A representation of an OAuth 2.0 Device Authorization Response. - * - * @author Steve Riesenberg - * @since 6.1 - * @see OAuth2DeviceCode - * @see OAuth2UserCode - * @see Section - * 3.2 Device Authorization Response - */ -public final class OAuth2DeviceAuthorizationResponse { - - private OAuth2DeviceCode deviceCode; - - private OAuth2UserCode userCode; - - private String verificationUri; - - private String verificationUriComplete; - - private long interval; - - private Map additionalParameters; - - private OAuth2DeviceAuthorizationResponse() { - } - - /** - * Returns the {@link OAuth2DeviceCode Device Code}. - * @return the {@link OAuth2DeviceCode} - */ - public OAuth2DeviceCode getDeviceCode() { - return this.deviceCode; - } - - /** - * Returns the {@link OAuth2UserCode User Code}. - * @return the {@link OAuth2UserCode} - */ - public OAuth2UserCode getUserCode() { - return this.userCode; - } - - /** - * Returns the end-user verification URI. - * @return the end-user verification URI - */ - public String getVerificationUri() { - return this.verificationUri; - } - - /** - * Returns the end-user verification URI that includes the user code. - * @return the end-user verification URI that includes the user code - */ - public String getVerificationUriComplete() { - return this.verificationUriComplete; - } - - /** - * Returns the minimum amount of time (in seconds) that the client should wait between - * polling requests to the token endpoint. - * @return the minimum amount of time between polling requests - */ - public long getInterval() { - return this.interval; - } - - /** - * Returns the additional parameters returned in the response. - * @return a {@code Map} of the additional parameters returned in the response, may be - * empty. - */ - public Map getAdditionalParameters() { - return this.additionalParameters; - } - - /** - * Returns a new {@link Builder}, initialized with the provided device code and user - * code values. - * @param deviceCode the value of the device code - * @param userCode the value of the user code - * @return the {@link Builder} - */ - public static Builder with(String deviceCode, String userCode) { - Assert.hasText(deviceCode, "deviceCode cannot be empty"); - Assert.hasText(userCode, "userCode cannot be empty"); - return new Builder(deviceCode, userCode); - } - - /** - * Returns a new {@link Builder}, initialized with the provided device code and user - * code. - * @param deviceCode the {@link OAuth2DeviceCode} - * @param userCode the {@link OAuth2UserCode} - * @return the {@link Builder} - */ - public static Builder with(OAuth2DeviceCode deviceCode, OAuth2UserCode userCode) { - Assert.notNull(deviceCode, "deviceCode cannot be null"); - Assert.notNull(userCode, "userCode cannot be null"); - return new Builder(deviceCode, userCode); - } - - /** - * A builder for {@link OAuth2DeviceAuthorizationResponse}. - */ - public static final class Builder { - - private final String deviceCode; - - private final String userCode; - - private String verificationUri; - - private String verificationUriComplete; - - private long expiresIn; - - private long interval; - - private Map additionalParameters; - - private Builder(OAuth2DeviceCode deviceCode, OAuth2UserCode userCode) { - this.deviceCode = deviceCode.getTokenValue(); - this.userCode = userCode.getTokenValue(); - this.expiresIn = ChronoUnit.SECONDS.between(deviceCode.getIssuedAt(), deviceCode.getExpiresAt()); - } - - private Builder(String deviceCode, String userCode) { - this.deviceCode = deviceCode; - this.userCode = userCode; - } - - /** - * Sets the end-user verification URI. - * @param verificationUri the end-user verification URI - * @return the {@link Builder} - */ - public Builder verificationUri(String verificationUri) { - this.verificationUri = verificationUri; - return this; - } - - /** - * Sets the end-user verification URI that includes the user code. - * @param verificationUriComplete the end-user verification URI that includes the - * user code - * @return the {@link Builder} - */ - public Builder verificationUriComplete(String verificationUriComplete) { - this.verificationUriComplete = verificationUriComplete; - return this; - } - - /** - * Sets the lifetime (in seconds) of the device code and user code. - * @param expiresIn the lifetime (in seconds) of the device code and user code - * @return the {@link Builder} - */ - public Builder expiresIn(long expiresIn) { - this.expiresIn = expiresIn; - return this; - } - - /** - * Sets the minimum amount of time (in seconds) that the client should wait - * between polling requests to the token endpoint. - * @param interval the minimum amount of time between polling requests - * @return the {@link Builder} - */ - public Builder interval(long interval) { - this.interval = interval; - return this; - } - - /** - * Sets the additional parameters returned in the response. - * @param additionalParameters the additional parameters returned in the response - * @return the {@link Builder} - */ - public Builder additionalParameters(Map additionalParameters) { - this.additionalParameters = additionalParameters; - return this; - } - - /** - * Builds a new {@link OAuth2DeviceAuthorizationResponse}. - * @return a {@link OAuth2DeviceAuthorizationResponse} - */ - public OAuth2DeviceAuthorizationResponse build() { - Assert.hasText(this.verificationUri, "verificationUri cannot be empty"); - Assert.isTrue(this.expiresIn > 0, "expiresIn must be greater than zero"); - - Instant issuedAt = Instant.now(); - Instant expiresAt = issuedAt.plusSeconds(this.expiresIn); - OAuth2DeviceCode deviceCode = new OAuth2DeviceCode(this.deviceCode, issuedAt, expiresAt); - OAuth2UserCode userCode = new OAuth2UserCode(this.userCode, issuedAt, expiresAt); - - OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse = new OAuth2DeviceAuthorizationResponse(); - deviceAuthorizationResponse.deviceCode = deviceCode; - deviceAuthorizationResponse.userCode = userCode; - deviceAuthorizationResponse.verificationUri = this.verificationUri; - deviceAuthorizationResponse.verificationUriComplete = this.verificationUriComplete; - deviceAuthorizationResponse.interval = this.interval; - deviceAuthorizationResponse.additionalParameters = Collections - .unmodifiableMap(CollectionUtils.isEmpty(this.additionalParameters) ? Collections.emptyMap() - : this.additionalParameters); - - return deviceAuthorizationResponse; - } - - } - -} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java index d387b482d9..ac1bb11e65 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ package org.springframework.security.oauth2.core.endpoint; * endpoint. * * @author Joe Grandja - * @author Steve Riesenberg * @since 5.0 * @see 11.2 * OAuth Parameters Registry @@ -151,37 +150,6 @@ public final class OAuth2ParameterNames { */ public static final String TOKEN_TYPE_HINT = "token_type_hint"; - /** - * {@code device_code} - used in Device Authorization Response and Device Access Token - * Request. - * @since 6.1 - */ - public static final String DEVICE_CODE = "device_code"; - - /** - * {@code user_code} - used in Device Authorization Response. - * @since 6.1 - */ - public static final String USER_CODE = "user_code"; - - /** - * {@code verification_uri} - used in Device Authorization Response. - * @since 6.1 - */ - public static final String VERIFICATION_URI = "verification_uri"; - - /** - * {@code verification_uri_complete} - used in Device Authorization Response. - * @since 6.1 - */ - public static final String VERIFICATION_URI_COMPLETE = "verification_uri_complete"; - - /** - * {@code interval} - used in Device Authorization Response. - * @since 6.1 - */ - public static final String INTERVAL = "interval"; - private OAuth2ParameterNames() { } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverter.java deleted file mode 100644 index a16551ecb4..0000000000 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverter.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.core.http.converter; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; - -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.convert.converter.Converter; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.MediaType; -import org.springframework.http.converter.AbstractHttpMessageConverter; -import org.springframework.http.converter.GenericHttpMessageConverter; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.security.oauth2.core.endpoint.OAuth2DeviceAuthorizationResponse; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; - -/** - * A {@link HttpMessageConverter} for an {@link OAuth2DeviceAuthorizationResponse OAuth - * 2.0 Device Authorization Response}. - * - * @author Steve Riesenberg - * @since 6.1 - * @see AbstractHttpMessageConverter - * @see OAuth2DeviceAuthorizationResponse - */ -public class OAuth2DeviceAuthorizationResponseHttpMessageConverter - extends AbstractHttpMessageConverter { - - private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() { - }; - - private final GenericHttpMessageConverter jsonMessageConverter = HttpMessageConverters - .getJsonMessageConverter(); - - private Converter, OAuth2DeviceAuthorizationResponse> deviceAuthorizationResponseConverter = new DefaultMapOAuth2DeviceAuthorizationResponseConverter(); - - private Converter> deviceAuthorizationResponseParametersConverter = new DefaultOAuth2DeviceAuthorizationResponseMapConverter(); - - @Override - protected boolean supports(Class clazz) { - return OAuth2DeviceAuthorizationResponse.class.isAssignableFrom(clazz); - } - - @Override - @SuppressWarnings("unchecked") - protected OAuth2DeviceAuthorizationResponse readInternal(Class clazz, - HttpInputMessage inputMessage) throws HttpMessageNotReadableException { - - try { - Map deviceAuthorizationResponseParameters = (Map) this.jsonMessageConverter - .read(STRING_OBJECT_MAP.getType(), null, inputMessage); - return this.deviceAuthorizationResponseConverter.convert(deviceAuthorizationResponseParameters); - } - catch (Exception ex) { - throw new HttpMessageNotReadableException( - "An error occurred reading the OAuth 2.0 Device Authorization Response: " + ex.getMessage(), ex, - inputMessage); - } - } - - @Override - protected void writeInternal(OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse, - HttpOutputMessage outputMessage) throws HttpMessageNotWritableException { - - try { - Map deviceAuthorizationResponseParameters = this.deviceAuthorizationResponseParametersConverter - .convert(deviceAuthorizationResponse); - this.jsonMessageConverter.write(deviceAuthorizationResponseParameters, STRING_OBJECT_MAP.getType(), - MediaType.APPLICATION_JSON, outputMessage); - } - catch (Exception ex) { - throw new HttpMessageNotWritableException( - "An error occurred writing the OAuth 2.0 Device Authorization Response: " + ex.getMessage(), ex); - } - } - - /** - * Sets the {@link Converter} used for converting the OAuth 2.0 Device Authorization - * Response parameters to an {@link OAuth2DeviceAuthorizationResponse}. - * @param deviceAuthorizationResponseConverter the {@link Converter} used for - * converting to an {@link OAuth2DeviceAuthorizationResponse} - */ - public final void setDeviceAuthorizationResponseConverter( - Converter, OAuth2DeviceAuthorizationResponse> deviceAuthorizationResponseConverter) { - Assert.notNull(deviceAuthorizationResponseConverter, "deviceAuthorizationResponseConverter cannot be null"); - this.deviceAuthorizationResponseConverter = deviceAuthorizationResponseConverter; - } - - /** - * Sets the {@link Converter} used for converting the - * {@link OAuth2DeviceAuthorizationResponse} to a {@code Map} representation of the - * OAuth 2.0 Device Authorization Response parameters. - * @param deviceAuthorizationResponseParametersConverter the {@link Converter} used - * for converting to a {@code Map} representation of the Device Authorization Response - * parameters - */ - public final void setDeviceAuthorizationResponseParametersConverter( - Converter> deviceAuthorizationResponseParametersConverter) { - Assert.notNull(deviceAuthorizationResponseParametersConverter, - "deviceAuthorizationResponseParametersConverter cannot be null"); - this.deviceAuthorizationResponseParametersConverter = deviceAuthorizationResponseParametersConverter; - } - - private static final class DefaultMapOAuth2DeviceAuthorizationResponseConverter - implements Converter, OAuth2DeviceAuthorizationResponse> { - - private static final Set DEVICE_AUTHORIZATION_RESPONSE_PARAMETER_NAMES = new HashSet<>( - Arrays.asList(OAuth2ParameterNames.DEVICE_CODE, OAuth2ParameterNames.USER_CODE, - OAuth2ParameterNames.VERIFICATION_URI, OAuth2ParameterNames.VERIFICATION_URI_COMPLETE, - OAuth2ParameterNames.EXPIRES_IN, OAuth2ParameterNames.INTERVAL)); - - @Override - public OAuth2DeviceAuthorizationResponse convert(Map parameters) { - String deviceCode = getParameterValue(parameters, OAuth2ParameterNames.DEVICE_CODE); - String userCode = getParameterValue(parameters, OAuth2ParameterNames.USER_CODE); - String verificationUri = getParameterValue(parameters, OAuth2ParameterNames.VERIFICATION_URI); - String verificationUriComplete = getParameterValue(parameters, - OAuth2ParameterNames.VERIFICATION_URI_COMPLETE); - long expiresIn = getParameterValue(parameters, OAuth2ParameterNames.EXPIRES_IN, 0L); - long interval = getParameterValue(parameters, OAuth2ParameterNames.INTERVAL, 0L); - Map additionalParameters = new LinkedHashMap<>(); - parameters.forEach((key, value) -> { - if (!DEVICE_AUTHORIZATION_RESPONSE_PARAMETER_NAMES.contains(key)) { - additionalParameters.put(key, value); - } - }); - // @formatter:off - return OAuth2DeviceAuthorizationResponse.with(deviceCode, userCode) - .verificationUri(verificationUri) - .verificationUriComplete(verificationUriComplete) - .expiresIn(expiresIn) - .interval(interval) - .additionalParameters(additionalParameters) - .build(); - // @formatter:on - } - - private static String getParameterValue(Map parameters, String parameterName) { - Object obj = parameters.get(parameterName); - return (obj != null) ? obj.toString() : null; - } - - private static long getParameterValue(Map parameters, String parameterName, long defaultValue) { - long parameterValue = defaultValue; - - Object obj = parameters.get(parameterName); - if (obj != null) { - // Final classes Long and Integer do not need to be coerced - if (obj.getClass() == Long.class) { - parameterValue = (Long) obj; - } - else if (obj.getClass() == Integer.class) { - parameterValue = (Integer) obj; - } - else { - // Attempt to coerce to a long (typically from a String) - try { - parameterValue = Long.parseLong(obj.toString()); - } - catch (NumberFormatException ignored) { - } - } - } - - return parameterValue; - } - - } - - private static final class DefaultOAuth2DeviceAuthorizationResponseMapConverter - implements Converter> { - - @Override - public Map convert(OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse) { - Map parameters = new HashMap<>(); - parameters.put(OAuth2ParameterNames.DEVICE_CODE, - deviceAuthorizationResponse.getDeviceCode().getTokenValue()); - parameters.put(OAuth2ParameterNames.USER_CODE, deviceAuthorizationResponse.getUserCode().getTokenValue()); - parameters.put(OAuth2ParameterNames.VERIFICATION_URI, deviceAuthorizationResponse.getVerificationUri()); - if (StringUtils.hasText(deviceAuthorizationResponse.getVerificationUriComplete())) { - parameters.put(OAuth2ParameterNames.VERIFICATION_URI_COMPLETE, - deviceAuthorizationResponse.getVerificationUriComplete()); - } - parameters.put(OAuth2ParameterNames.EXPIRES_IN, getExpiresIn(deviceAuthorizationResponse)); - if (deviceAuthorizationResponse.getInterval() > 0) { - parameters.put(OAuth2ParameterNames.INTERVAL, deviceAuthorizationResponse.getInterval()); - } - if (!CollectionUtils.isEmpty(deviceAuthorizationResponse.getAdditionalParameters())) { - parameters.putAll(deviceAuthorizationResponse.getAdditionalParameters()); - } - return parameters; - } - - private static long getExpiresIn(OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse) { - if (deviceAuthorizationResponse.getDeviceCode().getExpiresAt() != null) { - Instant issuedAt = (deviceAuthorizationResponse.getDeviceCode().getIssuedAt() != null) - ? deviceAuthorizationResponse.getDeviceCode().getIssuedAt() : Instant.now(); - return ChronoUnit.SECONDS.between(issuedAt, deviceAuthorizationResponse.getDeviceCode().getExpiresAt()); - } - return -1; - } - - } - -} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagersTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagersTests.java deleted file mode 100644 index e8e39d77c4..0000000000 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2AuthorizationManagersTests.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.core.authorization; - -import org.junit.jupiter.api.Test; - -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.core.Authentication; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Tests for {@link OAuth2AuthorizationManagers} - * - * @author Mario Petrovski - * @author Josh Cummings - */ -public class OAuth2AuthorizationManagersTests { - - @Test - void hasScopeWhenInvalidScopeThenThrowIllegalArgument() { - String scope = "SCOPE_invalid"; - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> OAuth2AuthorizationManagers.hasScope(scope)) - .withMessageContaining("SCOPE_invalid should not start with SCOPE_"); - } - - @Test - void hasAnyScopeWhenInvalidScopeThenThrowIllegalArgument() { - String[] scopes = { "read", "write", "SCOPE_invalid" }; - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> OAuth2AuthorizationManagers.hasAnyScope(scopes)) - .withMessageContaining("SCOPE_invalid should not start with SCOPE_"); - } - - @Test - void hasScopeWhenValidScopeThenAuthorizationManager() { - String scope = "read"; - AuthorizationManager authorizationManager = OAuth2AuthorizationManagers.hasScope(scope); - authorizationManager.verify(() -> hasScope(scope), new Object()); - assertThatExceptionOfType(AccessDeniedException.class) - .isThrownBy(() -> authorizationManager.verify(() -> hasScope("wrong"), new Object())); - } - - @Test - void hasAnyScopeWhenValidScopesThenAuthorizationManager() { - String[] scopes = { "read", "write" }; - AuthorizationManager authorizationManager = OAuth2AuthorizationManagers.hasAnyScope(scopes); - for (String scope : scopes) { - authorizationManager.verify(() -> hasScope(scope), new Object()); - } - assertThatExceptionOfType(AccessDeniedException.class) - .isThrownBy(() -> authorizationManager.verify(() -> hasScope("wrong"), new Object())); - } - - Authentication hasScope(String scope) { - return new TestingAuthenticationToken("user", "pass", "SCOPE_" + scope); - } - -} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2ReactiveAuthorizationManagersTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2ReactiveAuthorizationManagersTests.java deleted file mode 100644 index e2dd11c3c0..0000000000 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/authorization/OAuth2ReactiveAuthorizationManagersTests.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.core.authorization; - -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; - -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.authorization.ReactiveAuthorizationManager; -import org.springframework.security.core.Authentication; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Tests for {@link OAuth2ReactiveAuthorizationManagers} - * - * @author Josh Cummings - */ -public class OAuth2ReactiveAuthorizationManagersTests { - - @Test - void hasScopeWhenInvalidScopeThenThrowIllegalArgument() { - String scope = "SCOPE_invalid"; - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> OAuth2ReactiveAuthorizationManagers.hasScope(scope)) - .withMessageContaining("SCOPE_invalid should not start with SCOPE_"); - } - - @Test - void hasAnyScopeWhenInvalidScopeThenThrowIllegalArgument() { - String[] scopes = { "read", "write", "SCOPE_invalid" }; - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> OAuth2ReactiveAuthorizationManagers.hasAnyScope(scopes)) - .withMessageContaining("SCOPE_invalid should not start with SCOPE_"); - } - - @Test - void hasScopeWhenValidScopeThenAuthorizationManager() { - String scope = "read"; - ReactiveAuthorizationManager authorizationManager = OAuth2ReactiveAuthorizationManagers.hasScope(scope); - authorizationManager.verify(hasScope(scope), new Object()).block(); - assertThatExceptionOfType(AccessDeniedException.class) - .isThrownBy(() -> authorizationManager.verify(hasScope("wrong"), new Object()).block()); - } - - @Test - void hasAnyScopeWhenValidScopesThenAuthorizationManager() { - String[] scopes = { "read", "write" }; - ReactiveAuthorizationManager authorizationManager = OAuth2ReactiveAuthorizationManagers - .hasAnyScope(scopes); - for (String scope : scopes) { - authorizationManager.verify(hasScope(scope), new Object()).block(); - } - assertThatExceptionOfType(AccessDeniedException.class) - .isThrownBy(() -> authorizationManager.verify(hasScope("wrong"), new Object()).block()); - } - - Mono hasScope(String scope) { - return Mono.just(new TestingAuthenticationToken("user", "pass", "SCOPE_" + scope)); - } - -} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/converter/ClaimTypeConverterTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/converter/ClaimTypeConverterTests.java index 19bb064980..3b327a856b 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/converter/ClaimTypeConverterTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/converter/ClaimTypeConverterTests.java @@ -139,15 +139,15 @@ public class ClaimTypeConverterTests { claims.put(JSON_ARRAY_CLAIM, jsonArray); claims.put(JSON_OBJECT_CLAIM, jsonObject); claims = this.claimTypeConverter.convert(claims); - assertThat(claims).containsEntry(STRING_CLAIM, "true"); - assertThat(claims).containsEntry(BOOLEAN_CLAIM, Boolean.TRUE); - assertThat(claims).containsEntry(INSTANT_CLAIM, instant); - assertThat(claims).containsEntry(URL_CLAIM, url); - assertThat(claims).containsEntry(COLLECTION_STRING_CLAIM, listString); - assertThat(claims).containsEntry(LIST_STRING_CLAIM, listString); - assertThat(claims).containsEntry(MAP_STRING_OBJECT_CLAIM, mapStringObject); - assertThat(claims).containsEntry(JSON_ARRAY_CLAIM, jsonArrayListString); - assertThat(claims).containsEntry(JSON_OBJECT_CLAIM, jsonObjectMap); + assertThat(claims.get(STRING_CLAIM)).isEqualTo("true"); + assertThat(claims.get(BOOLEAN_CLAIM)).isEqualTo(Boolean.TRUE); + assertThat(claims.get(INSTANT_CLAIM)).isEqualTo(instant); + assertThat(claims.get(URL_CLAIM)).isEqualTo(url); + assertThat(claims.get(COLLECTION_STRING_CLAIM)).isEqualTo(listString); + assertThat(claims.get(LIST_STRING_CLAIM)).isEqualTo(listString); + assertThat(claims.get(MAP_STRING_OBJECT_CLAIM)).isEqualTo(mapStringObject); + assertThat(claims.get(JSON_ARRAY_CLAIM)).isEqualTo(jsonArrayListString); + assertThat(claims.get(JSON_OBJECT_CLAIM)).isEqualTo(jsonObjectMap); } @Test diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverterTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverterTests.java index 0b7433ffc0..7b07619d58 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverterTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverterTests.java @@ -22,6 +22,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,8 +30,6 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; -import static org.assertj.core.api.Assertions.assertThat; - /** * Tests for {@link DefaultMapOAuth2AccessTokenResponseConverter}. * @@ -57,24 +56,24 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests { map.put("custom_parameter_2", "custom-value-2"); OAuth2AccessTokenResponse converted = this.messageConverter.convert(map); OAuth2AccessToken accessToken = converted.getAccessToken(); - assertThat(accessToken).isNotNull(); - assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234"); - assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); + Assertions.assertNotNull(accessToken); + Assertions.assertEquals("access-token-1234", accessToken.getTokenValue()); + Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType()); Set scopes = accessToken.getScopes(); - assertThat(scopes).isNotNull(); - assertThat(scopes).hasSize(2); - assertThat(scopes).contains("read"); - assertThat(scopes).contains("write"); - assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()) - .isEqualTo(3600); + Assertions.assertNotNull(scopes); + Assertions.assertEquals(2, scopes.size()); + Assertions.assertTrue(scopes.contains("read")); + Assertions.assertTrue(scopes.contains("write")); + Assertions.assertEquals(3600, + Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()); OAuth2RefreshToken refreshToken = converted.getRefreshToken(); - assertThat(refreshToken).isNotNull(); - assertThat(refreshToken.getTokenValue()).isEqualTo("refresh-token-1234"); + Assertions.assertNotNull(refreshToken); + Assertions.assertEquals("refresh-token-1234", refreshToken.getTokenValue()); Map additionalParameters = converted.getAdditionalParameters(); - assertThat(additionalParameters).isNotNull(); - assertThat(additionalParameters).hasSize(2); - assertThat(additionalParameters).containsEntry("custom_parameter_1", "custom-value-1"); - assertThat(additionalParameters).containsEntry("custom_parameter_2", "custom-value-2"); + Assertions.assertNotNull(additionalParameters); + Assertions.assertEquals(2, additionalParameters.size()); + Assertions.assertEquals("custom-value-1", additionalParameters.get("custom_parameter_1")); + Assertions.assertEquals("custom-value-2", additionalParameters.get("custom_parameter_2")); } @Test @@ -84,18 +83,19 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests { map.put("token_type", "bearer"); OAuth2AccessTokenResponse converted = this.messageConverter.convert(map); OAuth2AccessToken accessToken = converted.getAccessToken(); - assertThat(accessToken).isNotNull(); - assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234"); - assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); + Assertions.assertNotNull(accessToken); + Assertions.assertEquals("access-token-1234", accessToken.getTokenValue()); + Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType()); Set scopes = accessToken.getScopes(); - assertThat(scopes).isNotNull(); - assertThat(scopes).isEmpty(); - assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()).isEqualTo(1); + Assertions.assertNotNull(scopes); + Assertions.assertEquals(0, scopes.size()); + Assertions.assertEquals(1, + Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()); OAuth2RefreshToken refreshToken = converted.getRefreshToken(); - assertThat(refreshToken).isNull(); + Assertions.assertNull(refreshToken); Map additionalParameters = converted.getAdditionalParameters(); - assertThat(additionalParameters).isNotNull(); - assertThat(additionalParameters).isEmpty(); + Assertions.assertNotNull(additionalParameters); + Assertions.assertEquals(0, additionalParameters.size()); } @Test @@ -106,18 +106,19 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests { map.put("expires_in", "2100-01-01-abc"); OAuth2AccessTokenResponse converted = this.messageConverter.convert(map); OAuth2AccessToken accessToken = converted.getAccessToken(); - assertThat(accessToken).isNotNull(); - assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234"); - assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); + Assertions.assertNotNull(accessToken); + Assertions.assertEquals("access-token-1234", accessToken.getTokenValue()); + Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType()); Set scopes = accessToken.getScopes(); - assertThat(scopes).isNotNull(); - assertThat(scopes).isEmpty(); - assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()).isEqualTo(1); + Assertions.assertNotNull(scopes); + Assertions.assertEquals(0, scopes.size()); + Assertions.assertEquals(1, + Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()); OAuth2RefreshToken refreshToken = converted.getRefreshToken(); - assertThat(refreshToken).isNull(); + Assertions.assertNull(refreshToken); Map additionalParameters = converted.getAdditionalParameters(); - assertThat(additionalParameters).isNotNull(); - assertThat(additionalParameters).isEmpty(); + Assertions.assertNotNull(additionalParameters); + Assertions.assertEquals(0, additionalParameters.size()); } // gh-9685 @@ -129,11 +130,11 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests { map.put("expires_in", 3600); OAuth2AccessTokenResponse converted = this.messageConverter.convert(map); OAuth2AccessToken accessToken = converted.getAccessToken(); - assertThat(accessToken).isNotNull(); - assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234"); - assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); - assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()) - .isEqualTo(3600); + Assertions.assertNotNull(accessToken); + Assertions.assertEquals("access-token-1234", accessToken.getTokenValue()); + Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType()); + Assertions.assertEquals(3600, + Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()); } // gh-9685 @@ -152,24 +153,24 @@ public class DefaultMapOAuth2AccessTokenResponseConverterTests { map.put("custom_parameter_2", "custom-value-2"); OAuth2AccessTokenResponse converted = this.messageConverter.convert(map); OAuth2AccessToken accessToken = converted.getAccessToken(); - assertThat(accessToken).isNotNull(); - assertThat(accessToken.getTokenValue()).isEqualTo("access-token-1234"); - assertThat(accessToken.getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); + Assertions.assertNotNull(accessToken); + Assertions.assertEquals("access-token-1234", accessToken.getTokenValue()); + Assertions.assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType()); Set scopes = accessToken.getScopes(); - assertThat(scopes).isNotNull(); - assertThat(scopes).hasSize(2); - assertThat(scopes).contains("read"); - assertThat(scopes).contains("write"); - assertThat(Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()) - .isEqualTo(3600); + Assertions.assertNotNull(scopes); + Assertions.assertEquals(2, scopes.size()); + Assertions.assertTrue(scopes.contains("read")); + Assertions.assertTrue(scopes.contains("write")); + Assertions.assertEquals(3600, + Duration.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()).getSeconds()); OAuth2RefreshToken refreshToken = converted.getRefreshToken(); - assertThat(refreshToken).isNotNull(); - assertThat(refreshToken.getTokenValue()).isEqualTo("refresh-token-1234"); + Assertions.assertNotNull(refreshToken); + Assertions.assertEquals("refresh-token-1234", refreshToken.getTokenValue()); Map additionalParameters = converted.getAdditionalParameters(); - assertThat(additionalParameters).isNotNull(); - assertThat(additionalParameters).hasSize(2); - assertThat(additionalParameters).containsEntry("custom_parameter_1", nestedObject); - assertThat(additionalParameters).containsEntry("custom_parameter_2", "custom-value-2"); + Assertions.assertNotNull(additionalParameters); + Assertions.assertEquals(2, additionalParameters.size()); + Assertions.assertEquals(nestedObject, additionalParameters.get("custom_parameter_1")); + Assertions.assertEquals("custom-value-2", additionalParameters.get("custom_parameter_2")); } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultOAuth2AccessTokenResponseMapConverterTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultOAuth2AccessTokenResponseMapConverterTests.java index 69a9ab3d95..0d58ddaea4 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultOAuth2AccessTokenResponseMapConverterTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/DefaultOAuth2AccessTokenResponseMapConverterTests.java @@ -22,14 +22,13 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.convert.converter.Converter; import org.springframework.security.oauth2.core.OAuth2AccessToken; -import static org.assertj.core.api.Assertions.assertThat; - /** * Tests for {@link DefaultOAuth2AccessTokenResponseMapConverter}. * @@ -62,14 +61,14 @@ public class DefaultOAuth2AccessTokenResponseMapConverterTests { .build(); // @formatter:on Map result = this.messageConverter.convert(build); - assertThat(result).hasSize(7); - assertThat(result).containsEntry("access_token", "access-token-value-1234"); - assertThat(result).containsEntry("refresh_token", "refresh-token-value-1234"); - assertThat(result).containsEntry("scope", "read write"); - assertThat(result).containsEntry("token_type", "Bearer"); - assertThat(result.get("expires_in")).isNotNull(); - assertThat(result).containsEntry("custom_parameter_1", "custom-value-1"); - assertThat(result).containsEntry("custom_parameter_2", "custom-value-2"); + Assertions.assertEquals(7, result.size()); + Assertions.assertEquals("access-token-value-1234", result.get("access_token")); + Assertions.assertEquals("refresh-token-value-1234", result.get("refresh_token")); + Assertions.assertEquals("read write", result.get("scope")); + Assertions.assertEquals("Bearer", result.get("token_type")); + Assertions.assertNotNull(result.get("expires_in")); + Assertions.assertEquals("custom-value-1", result.get("custom_parameter_1")); + Assertions.assertEquals("custom-value-2", result.get("custom_parameter_2")); } @Test @@ -80,10 +79,10 @@ public class DefaultOAuth2AccessTokenResponseMapConverterTests { .build(); // @formatter:on Map result = this.messageConverter.convert(build); - assertThat(result).hasSize(3); - assertThat(result).containsEntry("access_token", "access-token-value-1234"); - assertThat(result).containsEntry("token_type", "Bearer"); - assertThat(result.get("expires_in")).isNotNull(); + Assertions.assertEquals(3, result.size()); + Assertions.assertEquals("access-token-value-1234", result.get("access_token")); + Assertions.assertEquals("Bearer", result.get("token_type")); + Assertions.assertNotNull(result.get("expires_in")); } // gh-9685 @@ -108,14 +107,14 @@ public class DefaultOAuth2AccessTokenResponseMapConverterTests { .build(); // @formatter:on Map result = this.messageConverter.convert(build); - assertThat(result).hasSize(7); - assertThat(result).containsEntry("access_token", "access-token-value-1234"); - assertThat(result).containsEntry("refresh_token", "refresh-token-value-1234"); - assertThat(result).containsEntry("scope", "read write"); - assertThat(result).containsEntry("token_type", "Bearer"); - assertThat(result.get("expires_in")).isNotNull(); - assertThat(result).containsEntry("custom_parameter_1", nestedObject); - assertThat(result).containsEntry("custom_parameter_2", "custom-value-2"); + Assertions.assertEquals(7, result.size()); + Assertions.assertEquals("access-token-value-1234", result.get("access_token")); + Assertions.assertEquals("refresh-token-value-1234", result.get("refresh_token")); + Assertions.assertEquals("read write", result.get("scope")); + Assertions.assertEquals("Bearer", result.get("token_type")); + Assertions.assertNotNull(result.get("expires_in")); + Assertions.assertEquals(nestedObject, result.get("custom_parameter_1")); + Assertions.assertEquals("custom-value-2", result.get("custom_parameter_2")); } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverterTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverterTests.java deleted file mode 100644 index 9f834ee7cd..0000000000 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverterTests.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.core.http.converter; - -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.http.HttpStatus; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.mock.http.MockHttpOutputMessage; -import org.springframework.mock.http.client.MockClientHttpResponse; -import org.springframework.security.oauth2.core.endpoint.OAuth2DeviceAuthorizationResponse; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.entry; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link OAuth2DeviceAuthorizationResponseHttpMessageConverter}. - * - * @author Steve Riesenberg - */ -public class OAuth2DeviceAuthorizationResponseHttpMessageConverterTests { - - private OAuth2DeviceAuthorizationResponseHttpMessageConverter messageConverter; - - @BeforeEach - public void setup() { - this.messageConverter = new OAuth2DeviceAuthorizationResponseHttpMessageConverter(); - } - - @Test - public void supportsWhenOAuth2DeviceAuthorizationResponseThenTrue() { - assertThat(this.messageConverter.supports(OAuth2DeviceAuthorizationResponse.class)).isTrue(); - } - - @Test - public void setDeviceAuthorizationResponseConverterWhenConverterIsNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> this.messageConverter.setDeviceAuthorizationResponseConverter(null)); - } - - @Test - public void setDeviceAuthorizationResponseParametersConverterWhenConverterIsNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> this.messageConverter.setDeviceAuthorizationResponseParametersConverter(null)); - } - - @Test - public void readInternalWhenSuccessfulResponseWithAllParametersThenReadOAuth2DeviceAuthorizationResponse() { - // @formatter:off - String authorizationResponse = """ - { - "device_code": "GmRhm_DnyEy", - "user_code": "WDJB-MJHT", - "verification_uri": "https://example.com/device", - "verification_uri_complete": "https://example.com/device?user_code=WDJB-MJHT", - "expires_in": 1800, - "interval": 5, - "custom_parameter_1": "custom-value-1", - "custom_parameter_2": "custom-value-2" - } - """; - // @formatter:on - MockClientHttpResponse response = new MockClientHttpResponse(authorizationResponse.getBytes(), HttpStatus.OK); - OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse = this.messageConverter - .readInternal(OAuth2DeviceAuthorizationResponse.class, response); - assertThat(deviceAuthorizationResponse.getDeviceCode().getTokenValue()).isEqualTo("GmRhm_DnyEy"); - assertThat(deviceAuthorizationResponse.getDeviceCode().getIssuedAt()).isNotNull(); - assertThat(deviceAuthorizationResponse.getDeviceCode().getExpiresAt()) - .isBeforeOrEqualTo(Instant.now().plusSeconds(1800)); - assertThat(deviceAuthorizationResponse.getUserCode().getTokenValue()).isEqualTo("WDJB-MJHT"); - assertThat(deviceAuthorizationResponse.getUserCode().getIssuedAt()) - .isEqualTo(deviceAuthorizationResponse.getDeviceCode().getIssuedAt()); - assertThat(deviceAuthorizationResponse.getUserCode().getExpiresAt()) - .isEqualTo(deviceAuthorizationResponse.getDeviceCode().getExpiresAt()); - assertThat(deviceAuthorizationResponse.getVerificationUri()).isEqualTo("https://example.com/device"); - assertThat(deviceAuthorizationResponse.getVerificationUriComplete()) - .isEqualTo("https://example.com/device?user_code=WDJB-MJHT"); - assertThat(deviceAuthorizationResponse.getInterval()).isEqualTo(5); - assertThat(deviceAuthorizationResponse.getAdditionalParameters()).containsExactly( - entry("custom_parameter_1", "custom-value-1"), entry("custom_parameter_2", "custom-value-2")); - } - - @Test - public void readInternalWhenSuccessfulResponseWithNullValuesThenReadOAuth2DeviceAuthorizationResponse() { - // @formatter:off - String authorizationResponse = """ - { - "device_code": "GmRhm_DnyEy", - "user_code": "WDJB-MJHT", - "verification_uri": "https://example.com/device", - "verification_uri_complete": null, - "expires_in": 1800, - "interval": null - } - """; - // @formatter:on - MockClientHttpResponse response = new MockClientHttpResponse(authorizationResponse.getBytes(), HttpStatus.OK); - OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse = this.messageConverter - .readInternal(OAuth2DeviceAuthorizationResponse.class, response); - assertThat(deviceAuthorizationResponse.getDeviceCode().getTokenValue()).isEqualTo("GmRhm_DnyEy"); - assertThat(deviceAuthorizationResponse.getDeviceCode().getIssuedAt()).isNotNull(); - assertThat(deviceAuthorizationResponse.getDeviceCode().getExpiresAt()) - .isBeforeOrEqualTo(Instant.now().plusSeconds(1800)); - assertThat(deviceAuthorizationResponse.getUserCode().getTokenValue()).isEqualTo("WDJB-MJHT"); - assertThat(deviceAuthorizationResponse.getUserCode().getIssuedAt()) - .isEqualTo(deviceAuthorizationResponse.getDeviceCode().getIssuedAt()); - assertThat(deviceAuthorizationResponse.getUserCode().getExpiresAt()) - .isEqualTo(deviceAuthorizationResponse.getDeviceCode().getExpiresAt()); - assertThat(deviceAuthorizationResponse.getVerificationUri()).isEqualTo("https://example.com/device"); - assertThat(deviceAuthorizationResponse.getVerificationUriComplete()).isNull(); - assertThat(deviceAuthorizationResponse.getInterval()).isEqualTo(0); - } - - @Test - @SuppressWarnings("unchecked") - public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() { - Converter, OAuth2DeviceAuthorizationResponse> deviceAuthorizationResponseConverter = mock( - Converter.class); - given(deviceAuthorizationResponseConverter.convert(any())).willThrow(RuntimeException.class); - this.messageConverter.setDeviceAuthorizationResponseConverter(deviceAuthorizationResponseConverter); - String authorizationResponse = "{}"; - MockClientHttpResponse response = new MockClientHttpResponse(authorizationResponse.getBytes(), HttpStatus.OK); - assertThatExceptionOfType(HttpMessageNotReadableException.class) - .isThrownBy(() -> this.messageConverter.readInternal(OAuth2DeviceAuthorizationResponse.class, response)) - .withMessageContaining("An error occurred reading the OAuth 2.0 Device Authorization Response"); - } - - @Test - public void writeInternalWhenOAuth2DeviceAuthorizationResponseThenWriteResponse() { - Map additionalParameters = new HashMap<>(); - additionalParameters.put("custom_parameter_1", "custom-value-1"); - additionalParameters.put("custom_parameter_2", "custom-value-2"); - // @formatter:off - OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse = - OAuth2DeviceAuthorizationResponse.with("GmRhm_DnyEy", "WDJB-MJHT") - .verificationUri("https://example.com/device") - .verificationUriComplete("https://example.com/device?user_code=WDJB-MJHT") - .expiresIn(1800) - .interval(5) - .additionalParameters(additionalParameters) - .build(); - // @formatter:on - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - this.messageConverter.writeInternal(deviceAuthorizationResponse, outputMessage); - String authorizationResponse = outputMessage.getBodyAsString(); - assertThat(authorizationResponse).contains("\"device_code\":\"GmRhm_DnyEy\""); - assertThat(authorizationResponse).contains("\"user_code\":\"WDJB-MJHT\""); - assertThat(authorizationResponse).contains("\"verification_uri\":\"https://example.com/device\""); - assertThat(authorizationResponse) - .contains("\"verification_uri_complete\":\"https://example.com/device?user_code=WDJB-MJHT\""); - assertThat(authorizationResponse).contains("\"expires_in\":1800"); - assertThat(authorizationResponse).contains("\"interval\":5"); - assertThat(authorizationResponse).contains("\"custom_parameter_1\":\"custom-value-1\""); - assertThat(authorizationResponse).contains("\"custom_parameter_2\":\"custom-value-2\""); - } - - @Test - @SuppressWarnings("unchecked") - public void writeInternalWhenConversionFailsThenThrowHttpMessageNotWritableException() { - Converter> deviceAuthorizationResponseParametersConverter = mock( - Converter.class); - given(deviceAuthorizationResponseParametersConverter.convert(any())).willThrow(RuntimeException.class); - this.messageConverter - .setDeviceAuthorizationResponseParametersConverter(deviceAuthorizationResponseParametersConverter); - // @formatter:off - OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse = - OAuth2DeviceAuthorizationResponse.with("GmRhm_DnyEy", "WDJB-MJHT") - .verificationUri("https://example.com/device") - .expiresIn(1800) - .build(); - // @formatter:on - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - assertThatExceptionOfType(HttpMessageNotWritableException.class) - .isThrownBy(() -> this.messageConverter.writeInternal(deviceAuthorizationResponse, outputMessage)) - .withMessageContaining("An error occurred writing the OAuth 2.0 Device Authorization Response"); - } - -} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcIdTokenBuilderTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcIdTokenBuilderTests.java index d76edc05a4..50c02c7098 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcIdTokenBuilderTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcIdTokenBuilderTests.java @@ -41,11 +41,11 @@ public class OidcIdTokenBuilderTests { .build(); // @formatter:on assertThat(first.getClaims()).hasSize(1); - assertThat(first.getClaims()).containsEntry("TEST_CLAIM_1", "C1"); + assertThat(first.getClaims().get("TEST_CLAIM_1")).isEqualTo("C1"); assertThat(first.getTokenValue()).isEqualTo("V1"); assertThat(second.getClaims()).hasSize(2); - assertThat(second.getClaims()).containsEntry("TEST_CLAIM_1", "C2"); - assertThat(second.getClaims()).containsEntry("TEST_CLAIM_2", "C3"); + assertThat(second.getClaims().get("TEST_CLAIM_1")).isEqualTo("C2"); + assertThat(second.getClaims().get("TEST_CLAIM_2")).isEqualTo("C3"); assertThat(second.getTokenValue()).isEqualTo("V2"); } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcUserInfoBuilderTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcUserInfoBuilderTests.java index c41c9e03a5..50d7f56b1c 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcUserInfoBuilderTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/OidcUserInfoBuilderTests.java @@ -38,10 +38,10 @@ public class OidcUserInfoBuilderTests { .build(); // @formatter:on assertThat(first.getClaims()).hasSize(1); - assertThat(first.getClaims()).containsEntry("TEST_CLAIM_1", "C1"); + assertThat(first.getClaims().get("TEST_CLAIM_1")).isEqualTo("C1"); assertThat(second.getClaims()).hasSize(2); - assertThat(second.getClaims()).containsEntry("TEST_CLAIM_1", "C2"); - assertThat(second.getClaims()).containsEntry("TEST_CLAIM_2", "C3"); + assertThat(second.getClaims().get("TEST_CLAIM_1")).isEqualTo("C2"); + assertThat(second.getClaims().get("TEST_CLAIM_2")).isEqualTo("C3"); } @Test diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/TestOidcIdTokens.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/TestOidcIdTokens.java index 2271a52e00..ca859473d1 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/TestOidcIdTokens.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/TestOidcIdTokens.java @@ -17,7 +17,6 @@ package org.springframework.security.oauth2.core.oidc; import java.time.Instant; -import java.util.List; /** * Test {@link OidcIdToken}s @@ -33,7 +32,6 @@ public final class TestOidcIdTokens { // @formatter:off return OidcIdToken.withTokenValue("id-token") .issuer("https://example.com") - .audience(List.of("client-id")) .subject("subject") .issuedAt(Instant.now()) .expiresAt(Instant.now() diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java index ca2c37abf7..3bda7ec32d 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java @@ -50,7 +50,7 @@ public final class TestOidcUsers { .expiresAt(expiresAt) .subject("subject") .issuer("http://localhost/issuer") - .audience(Collections.unmodifiableSet(new LinkedHashSet<>(Collections.singletonList("client-id")))) + .audience(Collections.unmodifiableSet(new LinkedHashSet<>(Collections.singletonList("client")))) .authorizedParty("client") .build(); // @formatter:on diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JoseHeader.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JoseHeader.java index 3b749fe395..f40fb09a55 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JoseHeader.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JoseHeader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -353,7 +353,7 @@ class JoseHeader { private static URL convertAsURL(String header, String value) { URL convertedValue = ClaimConversionService.getSharedInstance().convert(value, URL.class); - Assert.notNull(convertedValue, + Assert.isTrue(convertedValue != null, () -> "Unable to convert header '" + header + "' of type '" + value.getClass() + "' to URL."); return convertedValue; } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java index 47a068dd75..3db496064e 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.util.Assert; import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestOperations; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; @@ -72,16 +71,12 @@ final class JwtDecoderProviderConfigurationUtils { } static Map getConfigurationForOidcIssuerLocation(String oidcIssuerLocation) { - return getConfiguration(oidcIssuerLocation, rest, oidc(URI.create(oidcIssuerLocation))); - } - - static Map getConfigurationForIssuerLocation(String issuer, RestOperations rest) { - URI uri = URI.create(issuer); - return getConfiguration(issuer, rest, oidc(uri), oidcRfc8414(uri), oauth(uri)); + return getConfiguration(oidcIssuerLocation, oidc(URI.create(oidcIssuerLocation))); } static Map getConfigurationForIssuerLocation(String issuer) { - return getConfigurationForIssuerLocation(issuer, rest); + URI uri = URI.create(issuer); + return getConfiguration(issuer, oidc(uri), oidcRfc8414(uri), oauth(uri)); } static void validateIssuer(Map configuration, String issuer) { @@ -149,7 +144,7 @@ final class JwtDecoderProviderConfigurationUtils { return "(unavailable)"; } - private static Map getConfiguration(String issuer, RestOperations rest, URI... uris) { + private static Map getConfiguration(String issuer, URI... uris) { String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + "\"" + issuer + "\""; for (URI uri : uris) { try { diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java index 0fe49708e7..8cc95ba1e3 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 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. @@ -89,10 +89,9 @@ public final class JwtDecoders { @SuppressWarnings("unchecked") public static T fromIssuerLocation(String issuer) { Assert.hasText(issuer, "issuer cannot be empty"); - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build(); - OAuth2TokenValidator jwtValidator = JwtValidators.createDefaultWithIssuer(issuer); - jwtDecoder.setJwtValidator(jwtValidator); - return (T) jwtDecoder; + Map configuration = JwtDecoderProviderConfigurationUtils + .getConfigurationForIssuerLocation(issuer); + return (T) withProviderConfiguration(configuration, issuer); } /** diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java index 01fa5b43e5..66b52e39ef 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,6 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import java.util.function.Function; import javax.crypto.SecretKey; @@ -194,38 +193,13 @@ public final class NimbusJwtDecoder implements JwtDecoder { private String getJwtValidationExceptionMessage(Collection errors) { for (OAuth2Error oAuth2Error : errors) { - if (StringUtils.hasLength(oAuth2Error.getDescription())) { + if (!StringUtils.isEmpty(oAuth2Error.getDescription())) { return String.format(DECODING_ERROR_MESSAGE_TEMPLATE, oAuth2Error.getDescription()); } } return "Unable to validate Jwt"; } - /** - * Use the given Issuer - * by making an OpenID - * Provider Configuration Request and using the values in the OpenID - * Provider Configuration Response to derive the needed - * JWK Set uri. - * @param issuer the Issuer - * @return a {@link JwkSetUriJwtDecoderBuilder} that will derive the JWK Set uri when - * {@link JwkSetUriJwtDecoderBuilder#build} is called - * @since 6.1 - * @see JwtDecoders - */ - public static JwkSetUriJwtDecoderBuilder withIssuerLocation(String issuer) { - return new JwkSetUriJwtDecoderBuilder((rest) -> { - Map configuration = JwtDecoderProviderConfigurationUtils - .getConfigurationForIssuerLocation(issuer, rest); - JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer); - return configuration.get("jwks_uri").toString(); - }, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms); - } - /** * Use the given JWK Set * uri. @@ -261,10 +235,7 @@ public final class NimbusJwtDecoder implements JwtDecoder { */ public static final class JwkSetUriJwtDecoderBuilder { - private Function jwkSetUri; - - private Function, Set> defaultAlgorithms = (source) -> Set - .of(JWSAlgorithm.RS256); + private String jwkSetUri; private Set signatureAlgorithms = new HashSet<>(); @@ -276,17 +247,7 @@ public final class NimbusJwtDecoder implements JwtDecoder { private JwkSetUriJwtDecoderBuilder(String jwkSetUri) { Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty"); - this.jwkSetUri = (rest) -> jwkSetUri; - this.jwtProcessorCustomizer = (processor) -> { - }; - } - - private JwkSetUriJwtDecoderBuilder(Function jwkSetUri, - Function, Set> defaultAlgorithms) { - Assert.notNull(jwkSetUri, "jwkSetUri function cannot be null"); - Assert.notNull(defaultAlgorithms, "defaultAlgorithms function cannot be null"); this.jwkSetUri = jwkSetUri; - this.defaultAlgorithms = defaultAlgorithms; this.jwtProcessorCustomizer = (processor) -> { }; } @@ -363,7 +324,7 @@ public final class NimbusJwtDecoder implements JwtDecoder { JWSKeySelector jwsKeySelector(JWKSource jwkSource) { if (this.signatureAlgorithms.isEmpty()) { - return new JWSVerificationKeySelector<>(this.defaultAlgorithms.apply(jwkSource), jwkSource); + return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource); } Set jwsAlgorithms = new HashSet<>(); for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) { @@ -373,18 +334,17 @@ public final class NimbusJwtDecoder implements JwtDecoder { return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource); } - JWKSource jwkSource(ResourceRetriever jwkSetRetriever, String jwkSetUri) { + JWKSource jwkSource(ResourceRetriever jwkSetRetriever) { if (this.cache == null) { - return new RemoteJWKSet<>(toURL(jwkSetUri), jwkSetRetriever); + return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever); } - JWKSetCache jwkSetCache = new SpringJWKSetCache(jwkSetUri, this.cache); - return new RemoteJWKSet<>(toURL(jwkSetUri), jwkSetRetriever, jwkSetCache); + JWKSetCache jwkSetCache = new SpringJWKSetCache(this.jwkSetUri, this.cache); + return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever, jwkSetCache); } JWTProcessor processor() { ResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever(this.restOperations); - String jwkSetUri = this.jwkSetUri.apply(this.restOperations); - JWKSource jwkSource = jwkSource(jwkSetRetriever, jwkSetUri); + JWKSource jwkSource = jwkSource(jwkSetRetriever); ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); jwtProcessor.setJWSKeySelector(jwsKeySelector(jwkSource)); // Spring Security validates the claim set independent from Nimbus @@ -473,7 +433,7 @@ public final class NimbusJwtDecoder implements JwtDecoder { HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON)); ResponseEntity response = getResponse(url, headers); - if (response.getStatusCode().value() != 200) { + if (response.getStatusCodeValue() != 200) { throw new IOException(response.toString()); } return new Resource(response.getBody(), "UTF-8"); diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java index 2da97ab96b..f5a98543fd 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java @@ -38,6 +38,7 @@ import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKMatcher; import com.nimbusds.jose.jwk.JWKSelector; import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.BadJOSEException; import com.nimbusds.jose.proc.JWKSecurityContext; import com.nimbusds.jose.proc.JWSKeySelector; @@ -205,45 +206,13 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { private String getJwtValidationExceptionMessage(Collection errors) { for (OAuth2Error oAuth2Error : errors) { - if (StringUtils.hasLength(oAuth2Error.getDescription())) { + if (!StringUtils.isEmpty(oAuth2Error.getDescription())) { return oAuth2Error.getDescription(); } } return "Unable to validate Jwt"; } - /** - * Use the given Issuer - * by making an OpenID - * Provider Configuration Request and using the values in the OpenID - * Provider Configuration Response to derive the needed - * JWK Set uri. - * @param issuer the Issuer - * @return a {@link NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder} that will derive the - * JWK Set uri when {@link NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder#build} is - * called - * @since 6.1 - * @see JwtDecoders - */ - public static JwkSetUriReactiveJwtDecoderBuilder withIssuerLocation(String issuer) { - return new JwkSetUriReactiveJwtDecoderBuilder( - (web) -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(issuer, web) - .flatMap((configuration) -> { - try { - JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer); - } - catch (IllegalStateException ex) { - return Mono.error(ex); - } - return Mono.just(configuration.get("jwks_uri").toString()); - }), - ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms); - } - /** * Use the given JWK Set * uri to validate JWTs. @@ -313,10 +282,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { private static final Duration FOREVER = Duration.ofMillis(Long.MAX_VALUE); - private Function> jwkSetUri; - - private Function>> defaultAlgorithms = (source) -> Mono - .just(Set.of(JWSAlgorithm.RS256)); + private final String jwkSetUri; private Set signatureAlgorithms = new HashSet<>(); @@ -326,16 +292,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { private JwkSetUriReactiveJwtDecoderBuilder(String jwkSetUri) { Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty"); - this.jwkSetUri = (web) -> Mono.just(jwkSetUri); - this.jwtProcessorCustomizer = (source, processor) -> Mono.just(processor); - } - - private JwkSetUriReactiveJwtDecoderBuilder(Function> jwkSetUri, - Function>> defaultAlgorithms) { - Assert.notNull(jwkSetUri, "jwkSetUri cannot be null"); - Assert.notNull(defaultAlgorithms, "defaultAlgorithms cannot be null"); this.jwkSetUri = jwkSetUri; - this.defaultAlgorithms = defaultAlgorithms; this.jwtProcessorCustomizer = (source, processor) -> Mono.just(processor); } @@ -414,32 +371,29 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { return new NimbusReactiveJwtDecoder(processor()); } - Mono> jwsKeySelector(ReactiveRemoteJWKSource source) { - JWKSecurityContextJWKSet jwkSource = new JWKSecurityContextJWKSet(); + JWSKeySelector jwsKeySelector(JWKSource jwkSource) { if (this.signatureAlgorithms.isEmpty()) { - return this.defaultAlgorithms.apply(source) - .map((algorithms) -> new JWSVerificationKeySelector<>(algorithms, jwkSource)); + return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource); } Set jwsAlgorithms = new HashSet<>(); for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) { JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName()); jwsAlgorithms.add(jwsAlgorithm); } - return Mono.just(new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource)); + return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource); } Converter> processor() { + JWKSecurityContextJWKSet jwkSource = new JWKSecurityContextJWKSet(); DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + JWSKeySelector jwsKeySelector = jwsKeySelector(jwkSource); + jwtProcessor.setJWSKeySelector(jwsKeySelector); jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); - ReactiveRemoteJWKSource source = new ReactiveRemoteJWKSource(this.jwkSetUri.apply(this.webClient)); + ReactiveRemoteJWKSource source = new ReactiveRemoteJWKSource(this.jwkSetUri); source.setWebClient(this.webClient); - Mono> jwsKeySelector = jwsKeySelector(source); - Mono, Function>> jwtProcessorMono = jwsKeySelector - .flatMap((selector) -> { - jwtProcessor.setJWSKeySelector(selector); - return this.jwtProcessorCustomizer.apply(source, jwtProcessor); - }) + Mono, Function>> jwtProcessorMono = this.jwtProcessorCustomizer + .apply(source, jwtProcessor) .map((processor) -> Tuples.of(processor, getExpectedJwsAlgorithms(processor.getJWSKeySelector()))) .cache((processor) -> FOREVER, (ex) -> Duration.ZERO, () -> Duration.ZERO); return (jwt) -> { @@ -542,7 +496,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); this.jwtProcessorCustomizer.accept(jwtProcessor); - return (jwt) -> Mono.fromCallable(() -> createClaimsSet(jwtProcessor, jwt, null)); + return (jwt) -> Mono.just(createClaimsSet(jwtProcessor, jwt, null)); } } @@ -617,7 +571,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); this.jwtProcessorCustomizer.accept(jwtProcessor); - return (jwt) -> Mono.fromCallable(() -> createClaimsSet(jwtProcessor, jwt, null)); + return (jwt) -> Mono.just(createClaimsSet(jwtProcessor, jwt, null)); } } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java index ad82339c2d..faf8e42c14 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,7 @@ package org.springframework.security.oauth2.jwt; -import java.net.URI; -import java.util.Collections; import java.util.HashSet; -import java.util.Map; import java.util.Set; import com.nimbusds.jose.JWSAlgorithm; @@ -34,24 +31,12 @@ import com.nimbusds.jose.proc.JWSKeySelector; import com.nimbusds.jose.proc.JWSVerificationKeySelector; import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.util.Assert; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.WebClientResponseException; -import org.springframework.web.util.UriComponentsBuilder; final class ReactiveJwtDecoderProviderConfigurationUtils { - private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration"; - - private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server"; - - private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference>() { - }; - static Mono> addJWSAlgorithms( ReactiveRemoteJWKSource jwkSource, ConfigurableJWTProcessor jwtProcessor) { JWSKeySelector selector = jwtProcessor.getJWSKeySelector(); @@ -89,56 +74,7 @@ final class ReactiveJwtDecoderProviderConfigurationUtils { } Assert.notEmpty(jwsAlgorithms, "Failed to find any algorithms from the JWK set"); return jwsAlgorithms; - }).onErrorMap(KeySourceException.class, IllegalStateException::new); - } - - static Mono> getConfigurationForIssuerLocation(String issuer, WebClient web) { - URI uri = URI.create(issuer); - return getConfiguration(issuer, web, oidc(uri), oidcRfc8414(uri), oauth(uri)); - } - - private static URI oidc(URI issuer) { - // @formatter:off - return UriComponentsBuilder.fromUri(issuer) - .replacePath(issuer.getPath() + OIDC_METADATA_PATH) - .build(Collections.emptyMap()); - // @formatter:on - } - - private static URI oidcRfc8414(URI issuer) { - // @formatter:off - return UriComponentsBuilder.fromUri(issuer) - .replacePath(OIDC_METADATA_PATH + issuer.getPath()) - .build(Collections.emptyMap()); - // @formatter:on - } - - private static URI oauth(URI issuer) { - // @formatter:off - return UriComponentsBuilder.fromUri(issuer) - .replacePath(OAUTH_METADATA_PATH + issuer.getPath()) - .build(Collections.emptyMap()); - // @formatter:on - } - - private static Mono> getConfiguration(String issuer, WebClient web, URI... uris) { - String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + "\"" + issuer + "\""; - return Flux.just(uris) - .concatMap((uri) -> web.get().uri(uri).retrieve().bodyToMono(STRING_OBJECT_MAP)) - .flatMap((configuration) -> { - if (configuration.get("jwks_uri") == null) { - return Mono.error(() -> new IllegalArgumentException("The public JWK set URI must not be null")); - } - return Mono.just(configuration); - }) - .onErrorContinue((ex) -> ex instanceof WebClientResponseException - && ((WebClientResponseException) ex).getStatusCode().is4xxClientError(), (ex, object) -> { - }) - .onErrorMap(RuntimeException.class, - (ex) -> (ex instanceof IllegalArgumentException) ? ex - : new IllegalArgumentException(errorMessage, ex)) - .next() - .switchIfEmpty(Mono.error(() -> new IllegalArgumentException(errorMessage))); + }).onErrorMap(KeySourceException.class, (ex) -> new IllegalStateException(ex)); } private ReactiveJwtDecoderProviderConfigurationUtils() { diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtBuilderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtBuilderTests.java index 2d99766254..09d13a6e7a 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtBuilderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtBuilderTests.java @@ -47,16 +47,16 @@ public class JwtBuilderTests { .build(); // @formatter:on assertThat(first.getHeaders()).hasSize(1); - assertThat(first.getHeaders()).containsEntry("TEST_HEADER_1", "H1"); + assertThat(first.getHeaders().get("TEST_HEADER_1")).isEqualTo("H1"); assertThat(first.getClaims()).hasSize(1); - assertThat(first.getClaims()).containsEntry("TEST_CLAIM_1", "C1"); + assertThat(first.getClaims().get("TEST_CLAIM_1")).isEqualTo("C1"); assertThat(first.getTokenValue()).isEqualTo("V1"); assertThat(second.getHeaders()).hasSize(2); - assertThat(second.getHeaders()).containsEntry("TEST_HEADER_1", "H2"); - assertThat(second.getHeaders()).containsEntry("TEST_HEADER_2", "H3"); + assertThat(second.getHeaders().get("TEST_HEADER_1")).isEqualTo("H2"); + assertThat(second.getHeaders().get("TEST_HEADER_2")).isEqualTo("H3"); assertThat(second.getClaims()).hasSize(2); - assertThat(second.getClaims()).containsEntry("TEST_CLAIM_1", "C2"); - assertThat(second.getClaims()).containsEntry("TEST_CLAIM_2", "C3"); + assertThat(second.getClaims().get("TEST_CLAIM_1")).isEqualTo("C2"); + assertThat(second.getClaims().get("TEST_CLAIM_2")).isEqualTo("C3"); assertThat(second.getTokenValue()).isEqualTo("V2"); } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java index 9da90cfd0d..a43989c868 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java @@ -50,7 +50,7 @@ public class JwtClaimValidatorTests { public void validateWhenClaimFailsTheTestThenReturnsFailure() { Jwt jwt = TestJwts.jwt().claim(JwtClaimNames.ISS, "http://abc").build(); Collection details = this.validator.validate(jwt).getErrors(); - assertThat(this.validator.validate(jwt).getErrors()).isNotEmpty(); + assertThat(this.validator.validate(jwt).getErrors().isEmpty()).isFalse(); assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN)); } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/MappedJwtClaimSetConverterTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/MappedJwtClaimSetConverterTests.java index f07bbd9df1..9c365411b8 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/MappedJwtClaimSetConverterTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/MappedJwtClaimSetConverterTests.java @@ -53,7 +53,7 @@ public class MappedJwtClaimSetConverterTests { .withDefaults(Collections.singletonMap(JwtClaimNames.EXP, expiresAtConverter)); Map source = new HashMap<>(); Map target = converter.convert(source); - assertThat(target).containsEntry(JwtClaimNames.IAT, Instant.ofEpochMilli(at.toEpochMilli()).minusSeconds(1)); + assertThat(target.get(JwtClaimNames.IAT)).isEqualTo(Instant.ofEpochMilli(at.toEpochMilli()).minusSeconds(1)); } @Test @@ -61,8 +61,8 @@ public class MappedJwtClaimSetConverterTests { MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()); Map source = Collections.singletonMap(JwtClaimNames.EXP, 1000000000L); Map target = converter.convert(source); - assertThat(target).containsEntry(JwtClaimNames.EXP, Instant.ofEpochSecond(1000000000L)); - assertThat(target).containsEntry(JwtClaimNames.IAT, Instant.ofEpochSecond(1000000000L).minusSeconds(1)); + assertThat(target.get(JwtClaimNames.EXP)).isEqualTo(Instant.ofEpochSecond(1000000000L)); + assertThat(target.get(JwtClaimNames.IAT)).isEqualTo(Instant.ofEpochSecond(1000000000L).minusSeconds(1)); } @Test @@ -71,11 +71,11 @@ public class MappedJwtClaimSetConverterTests { Map source = Collections.singletonMap(JwtClaimNames.AUD, "audience"); Map target = converter.convert(source); assertThat(target.get(JwtClaimNames.AUD)).isInstanceOf(Collection.class); - assertThat(target).containsEntry(JwtClaimNames.AUD, Arrays.asList("audience")); + assertThat(target.get(JwtClaimNames.AUD)).isEqualTo(Arrays.asList("audience")); source = Collections.singletonMap(JwtClaimNames.AUD, Arrays.asList("one", "two")); target = converter.convert(source); assertThat(target.get(JwtClaimNames.AUD)).isInstanceOf(Collection.class); - assertThat(target).containsEntry(JwtClaimNames.AUD, Arrays.asList("one", "two")); + assertThat(target.get(JwtClaimNames.AUD)).isEqualTo(Arrays.asList("one", "two")); } @Test @@ -90,13 +90,13 @@ public class MappedJwtClaimSetConverterTests { source.put(JwtClaimNames.NBF, 1000000000); source.put(JwtClaimNames.SUB, 1234); Map target = converter.convert(source); - assertThat(target).containsEntry(JwtClaimNames.JTI, "1"); - assertThat(target).containsEntry(JwtClaimNames.AUD, Arrays.asList("audience")); - assertThat(target).containsEntry(JwtClaimNames.EXP, Instant.ofEpochSecond(2000000000L)); - assertThat(target).containsEntry(JwtClaimNames.IAT, Instant.ofEpochSecond(1000000000L)); - assertThat(target).containsEntry(JwtClaimNames.ISS, "https://any.url"); - assertThat(target).containsEntry(JwtClaimNames.NBF, Instant.ofEpochSecond(1000000000L)); - assertThat(target).containsEntry(JwtClaimNames.SUB, "1234"); + assertThat(target.get(JwtClaimNames.JTI)).isEqualTo("1"); + assertThat(target.get(JwtClaimNames.AUD)).isEqualTo(Arrays.asList("audience")); + assertThat(target.get(JwtClaimNames.EXP)).isEqualTo(Instant.ofEpochSecond(2000000000L)); + assertThat(target.get(JwtClaimNames.IAT)).isEqualTo(Instant.ofEpochSecond(1000000000L)); + assertThat(target.get(JwtClaimNames.ISS)).isEqualTo("https://any.url"); + assertThat(target.get(JwtClaimNames.NBF)).isEqualTo(Instant.ofEpochSecond(1000000000L)); + assertThat(target.get(JwtClaimNames.SUB)).isEqualTo("1234"); } @Test @@ -114,13 +114,13 @@ public class MappedJwtClaimSetConverterTests { source.put(JwtClaimNames.NBF, "1000000000"); source.put(JwtClaimNames.SUB, 2345); Map target = converter.convert(source); - assertThat(target).containsEntry(JwtClaimNames.JTI, "1"); - assertThat(target).containsEntry(JwtClaimNames.AUD, Arrays.asList("audience")); - assertThat(target).containsEntry(JwtClaimNames.EXP, Instant.ofEpochSecond(2000000000L)); - assertThat(target).containsEntry(JwtClaimNames.IAT, Instant.ofEpochSecond(1000000000L)); - assertThat(target).containsEntry(JwtClaimNames.ISS, "https://any.url"); - assertThat(target).containsEntry(JwtClaimNames.NBF, Instant.ofEpochSecond(1000000000L)); - assertThat(target).containsEntry(JwtClaimNames.SUB, "1234"); + assertThat(target.get(JwtClaimNames.JTI)).isEqualTo("1"); + assertThat(target.get(JwtClaimNames.AUD)).isEqualTo(Arrays.asList("audience")); + assertThat(target.get(JwtClaimNames.EXP)).isEqualTo(Instant.ofEpochSecond(2000000000L)); + assertThat(target.get(JwtClaimNames.IAT)).isEqualTo(Instant.ofEpochSecond(1000000000L)); + assertThat(target.get(JwtClaimNames.ISS)).isEqualTo("https://any.url"); + assertThat(target.get(JwtClaimNames.NBF)).isEqualTo(Instant.ofEpochSecond(1000000000L)); + assertThat(target.get(JwtClaimNames.SUB)).isEqualTo("1234"); } // gh-10135 @@ -149,7 +149,7 @@ public class MappedJwtClaimSetConverterTests { given(claimConverter.convert(any())).willReturn("custom-value"); Map source = new HashMap<>(); Map target = converter.convert(source); - assertThat(target).containsEntry("custom-claim", "custom-value"); + assertThat(target.get("custom-claim")).isEqualTo("custom-value"); } @Test @@ -167,13 +167,13 @@ public class MappedJwtClaimSetConverterTests { source.put(JwtClaimNames.NBF, new Object()); source.put(JwtClaimNames.SUB, new Object()); Map target = converter.convert(source); - assertThat(target).containsEntry(JwtClaimNames.JTI, source.get(JwtClaimNames.JTI)); - assertThat(target).containsEntry(JwtClaimNames.AUD, source.get(JwtClaimNames.AUD)); - assertThat(target).containsEntry(JwtClaimNames.EXP, source.get(JwtClaimNames.EXP)); - assertThat(target).containsEntry(JwtClaimNames.IAT, source.get(JwtClaimNames.IAT)); - assertThat(target).containsEntry(JwtClaimNames.ISS, source.get(JwtClaimNames.ISS)); - assertThat(target).containsEntry(JwtClaimNames.NBF, source.get(JwtClaimNames.NBF)); - assertThat(target).containsEntry(JwtClaimNames.SUB, "1234"); + assertThat(target.get(JwtClaimNames.JTI)).isEqualTo(source.get(JwtClaimNames.JTI)); + assertThat(target.get(JwtClaimNames.AUD)).isEqualTo(source.get(JwtClaimNames.AUD)); + assertThat(target.get(JwtClaimNames.EXP)).isEqualTo(source.get(JwtClaimNames.EXP)); + assertThat(target.get(JwtClaimNames.IAT)).isEqualTo(source.get(JwtClaimNames.IAT)); + assertThat(target.get(JwtClaimNames.ISS)).isEqualTo(source.get(JwtClaimNames.ISS)); + assertThat(target.get(JwtClaimNames.NBF)).isEqualTo(source.get(JwtClaimNames.NBF)); + assertThat(target.get(JwtClaimNames.SUB)).isEqualTo("1234"); } @Test @@ -195,7 +195,7 @@ public class MappedJwtClaimSetConverterTests { MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()); Map nonUriIssuer = Collections.singletonMap(JwtClaimNames.ISS, "issuer"); Map target = converter.convert(nonUriIssuer); - assertThat(target).containsEntry(JwtClaimNames.ISS, "issuer"); + assertThat(target.get(JwtClaimNames.ISS)).isEqualTo("issuer"); } // gh-6073 @@ -204,7 +204,7 @@ public class MappedJwtClaimSetConverterTests { MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()); Map issuer = Collections.singletonMap(JwtClaimNames.ISS, new URL("https://issuer")); Map target = converter.convert(issuer); - assertThat(target).containsEntry(JwtClaimNames.ISS, "https://issuer"); + assertThat(target.get(JwtClaimNames.ISS)).isEqualTo("https://issuer"); } @Test diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJweEncoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJweEncoderTests.java index 5aae9a8f22..2bc95d6495 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJweEncoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJweEncoderTests.java @@ -100,11 +100,11 @@ public class NimbusJweEncoderTests { // @formatter:on Jwt encodedJwe = this.jweEncoder.encode(JwtEncoderParameters.from(jwtClaimsSet)); - assertThat(encodedJwe.getHeaders()).containsEntry(JoseHeaderNames.ALG, DEFAULT_JWE_HEADER.getAlgorithm()); - assertThat(encodedJwe.getHeaders()).containsEntry("enc", DEFAULT_JWE_HEADER.getHeader("enc")); + assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(DEFAULT_JWE_HEADER.getAlgorithm()); + assertThat(encodedJwe.getHeaders().get("enc")).isEqualTo(DEFAULT_JWE_HEADER.getHeader("enc")); assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.JKU)).isNull(); assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.JWK)).isNull(); - assertThat(encodedJwe.getHeaders()).containsEntry(JoseHeaderNames.KID, rsaJwk.getKeyID()); + assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(rsaJwk.getKeyID()); assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.X5U)).isNull(); assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.X5C)).isNull(); assertThat(encodedJwe.getHeaders().get(JoseHeaderNames.X5T)).isNull(); @@ -144,18 +144,18 @@ public class NimbusJweEncoderTests { // @formatter:on Jwt encodedJweNestedJws = this.jweEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet)); - assertThat(encodedJweNestedJws.getHeaders()).containsEntry(JoseHeaderNames.ALG, - DEFAULT_JWE_HEADER.getAlgorithm()); - assertThat(encodedJweNestedJws.getHeaders()).containsEntry("enc", DEFAULT_JWE_HEADER.getHeader("enc")); + assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.ALG)) + .isEqualTo(DEFAULT_JWE_HEADER.getAlgorithm()); + assertThat(encodedJweNestedJws.getHeaders().get("enc")).isEqualTo(DEFAULT_JWE_HEADER.getHeader("enc")); assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.JKU)).isNull(); assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.JWK)).isNull(); - assertThat(encodedJweNestedJws.getHeaders()).containsEntry(JoseHeaderNames.KID, rsaJwk.getKeyID()); + assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(rsaJwk.getKeyID()); assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.X5U)).isNull(); assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.X5C)).isNull(); assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.X5T)).isNull(); assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.X5T_S256)).isNull(); assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.TYP)).isNull(); - assertThat(encodedJweNestedJws.getHeaders()).containsEntry(JoseHeaderNames.CTY, "JWT"); + assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.CTY)).isEqualTo("JWT"); assertThat(encodedJweNestedJws.getHeaders().get(JoseHeaderNames.CRIT)).isNull(); assertThat(encodedJweNestedJws.getIssuer()).isEqualTo(jwtClaimsSet.getIssuer()); diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java index 4a709e09c1..2109ddd6b7 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,6 @@ import org.mockito.ArgumentCaptor; import org.springframework.cache.Cache; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.cache.support.SimpleValueWrapper; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -98,7 +97,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; */ public class NimbusJwtDecoderTests { - private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; + private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; private static final String NEW_KID_JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"two\",\"n\":\"ra9UJw4I0fCHuOqr1xWJsh-qcVeZWtKEU3uoqq1sAg5fG67dujNCm_Q16yuO0ZdDiU0vlJkbc_MXFAvm4ZxdJ_qR7PAneV-BOGNtLpSaiPclscCy3m7zjRWkaqwt9ZZEsdK5UqXyPlBpcYhNKsmnQGjnX4sYb7d8b2jSCM_qto48-6451rbyEhXXywtFy_JqtTpbsw_IIdQHMr1O-MdSjsQxX9kkvZwPU8LsC-CcqlcsZ7mnpOhmIXaf4tbRwAaluXwYft0yykFsp8e5C4t9mMs9Vu8AB5gT8o-D_ovXd2qh4k3ejzVpYLtzD4nbfvPJA_TXmjhn-9GOPAqkzfON2Q\"}]}"; @@ -248,8 +247,8 @@ public class NimbusJwtDecoderTests { given(claimSetConverter.convert(any(Map.class))).willReturn(Collections.singletonMap("custom", "value")); this.jwtDecoder.setClaimSetConverter(claimSetConverter); Jwt jwt = this.jwtDecoder.decode(SIGNED_JWT); - assertThat(jwt.getClaims()).hasSize(1); - assertThat(jwt.getClaims()).containsEntry("custom", "value"); + assertThat(jwt.getClaims().size()).isEqualTo(1); + assertThat(jwt.getClaims().get("custom")).isEqualTo("value"); } // gh-7885 @@ -313,19 +312,6 @@ public class NimbusJwtDecoderTests { } } - @Test - public void decodeWhenIssuerLocationThenOk() { - String issuer = "https://example.org/issuer"; - RestOperations restOperations = mock(RestOperations.class); - given(restOperations.exchange(any(RequestEntity.class), any(ParameterizedTypeReference.class))) - .willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK)); - given(restOperations.exchange(any(RequestEntity.class), eq(String.class))) - .willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK)); - JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build(); - Jwt jwt = jwtDecoder.decode(SIGNED_JWT); - assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull(); - } - @Test public void withJwkSetUriWhenNullOrEmptyThenThrowsException() { // @formatter:off diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoderTests.java index e9825f0a35..526bc511a8 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoderTests.java @@ -144,7 +144,7 @@ public class NimbusJwtEncoderTests { Jwt encodedJws = this.jwtEncoder.encode(JwtEncoderParameters.from(jwtClaimsSet)); - assertThat(encodedJws.getHeaders()).containsEntry(JoseHeaderNames.ALG, SignatureAlgorithm.RS256); + assertThat(encodedJws.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(SignatureAlgorithm.RS256); } @Test @@ -165,7 +165,7 @@ public class NimbusJwtEncoderTests { Jwt encodedJws = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet)); - assertThat(encodedJws.getHeaders()).containsEntry(JoseHeaderNames.KID, rsaJwk2.getKeyID()); + assertThat(encodedJws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(rsaJwk2.getKeyID()); } @Test @@ -190,8 +190,8 @@ public class NimbusJwtEncoderTests { Jwt encodedJws = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet)); - assertThat(encodedJws.getHeaders()).containsEntry(JoseHeaderNames.X5T_S256, - rsaJwk1.getX509CertSHA256Thumbprint().toString()); + assertThat(encodedJws.getHeaders().get(JoseHeaderNames.X5T_S256)) + .isEqualTo(rsaJwk1.getX509CertSHA256Thumbprint().toString()); assertThat(encodedJws.getHeaders().get(JoseHeaderNames.KID)).isNull(); } @@ -231,15 +231,15 @@ public class NimbusJwtEncoderTests { Jwt encodedJws = this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet)); - assertThat(encodedJws.getHeaders()).containsEntry(JoseHeaderNames.ALG, jwsHeader.getAlgorithm()); + assertThat(encodedJws.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(jwsHeader.getAlgorithm()); assertThat(encodedJws.getHeaders().get(JoseHeaderNames.JKU)).isNull(); assertThat(encodedJws.getHeaders().get(JoseHeaderNames.JWK)).isNull(); - assertThat(encodedJws.getHeaders()).containsEntry(JoseHeaderNames.KID, rsaJwk.getKeyID()); + assertThat(encodedJws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(rsaJwk.getKeyID()); assertThat(encodedJws.getHeaders().get(JoseHeaderNames.X5U)).isNull(); assertThat(encodedJws.getHeaders().get(JoseHeaderNames.X5C)).isNull(); assertThat(encodedJws.getHeaders().get(JoseHeaderNames.X5T)).isNull(); - assertThat(encodedJws.getHeaders()).containsEntry(JoseHeaderNames.X5T_S256, - rsaJwk.getX509CertSHA256Thumbprint().toString()); + assertThat(encodedJws.getHeaders().get(JoseHeaderNames.X5T_S256)) + .isEqualTo(rsaJwk.getX509CertSHA256Thumbprint().toString()); assertThat(encodedJws.getHeaders().get(JoseHeaderNames.TYP)).isNull(); assertThat(encodedJws.getHeaders().get(JoseHeaderNames.CTY)).isNull(); assertThat(encodedJws.getHeaders().get(JoseHeaderNames.CRIT)).isNull(); diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java index d5f2ca9b2c..2d7093b06b 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java @@ -41,6 +41,7 @@ import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; import com.nimbusds.jose.proc.JWKSecurityContext; import com.nimbusds.jose.proc.JWSKeySelector; @@ -57,7 +58,6 @@ import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.convert.converter.Converter; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2TokenValidator; @@ -149,7 +149,7 @@ public class NimbusReactiveJwtDecoderTests { @Test public void decodeWhenMessageReadScopeThenSuccess() { Jwt jwt = this.decoder.decode(this.messageReadToken).block(); - assertThat(jwt.getClaims()).containsEntry("scope", "message:read"); + assertThat(jwt.getClaims().get("scope")).isEqualTo("message:read"); } @Test @@ -167,7 +167,7 @@ public class NimbusReactiveJwtDecoderTests { public void decodeWhenIssuedAtThenSuccess() { String withIssuedAt = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6IiIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NSwiaWF0IjoxNTI5OTQyNDQ4fQ.LBzAJO-FR-uJDHST61oX4kimuQjz6QMJPW_mvEXRB6A-fMQWpfTQ089eboipAqsb33XnwWth9ELju9HMWLk0FjlWVVzwObh9FcoKelmPNR8mZIlFG-pAYGgSwi8HufyLabXHntFavBiFtqwp_z9clSOFK1RxWvt3lywEbGgtCKve0BXOjfKWiH1qe4QKGixH-NFxidvz8Qd5WbJwyb9tChC6ZKoKPv7Jp-N5KpxkY-O2iUtINvn4xOSactUsvKHgF8ZzZjvJGzG57r606OZXaNtoElQzjAPU5xDGg5liuEJzfBhvqiWCLRmSuZ33qwp3aoBnFgEw0B85gsNe3ggABg"; Jwt jwt = this.decoder.decode(withIssuedAt).block(); - assertThat(jwt.getClaims()).containsEntry(JwtClaimNames.IAT, Instant.ofEpochSecond(1529942448L)); + assertThat(jwt.getClaims().get(JwtClaimNames.IAT)).isEqualTo(Instant.ofEpochSecond(1529942448L)); } @Test @@ -268,8 +268,8 @@ public class NimbusReactiveJwtDecoderTests { this.decoder.setClaimSetConverter(claimSetConverter); given(claimSetConverter.convert(any(Map.class))).willReturn(Collections.singletonMap("custom", "value")); Jwt jwt = this.decoder.decode(this.messageReadToken).block(); - assertThat(jwt.getClaims()).hasSize(1); - assertThat(jwt.getClaims()).containsEntry("custom", "value"); + assertThat(jwt.getClaims().size()).isEqualTo(1); + assertThat(jwt.getClaims().get("custom")).isEqualTo("value"); verify(claimSetConverter).convert(any(Map.class)); } @@ -601,31 +601,11 @@ public class NimbusReactiveJwtDecoderTests { // @formatter:on } - @Test - public void decodeWhenIssuerLocationThenOk() { - String issuer = "https://example.org/issuer"; - WebClient real = WebClient.builder().build(); - WebClient.RequestHeadersUriSpec spec = spy(real.get()); - WebClient webClient = spy(WebClient.class); - given(webClient.get()).willReturn(spec); - WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class); - given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(this.jwkSet)); - given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class))) - .willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"))); - given(spec.retrieve()).willReturn(responseSpec); - ReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer) - .webClient(webClient) - .build(); - Jwt jwt = jwtDecoder.decode(this.messageReadToken).block(); - assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull(); - } - @Test public void jwsKeySelectorWhenNoAlgorithmThenReturnsRS256Selector() { - ReactiveRemoteJWKSource jwkSource = mock(ReactiveRemoteJWKSource.class); + JWKSource jwkSource = mock(JWKSource.class); JWSKeySelector jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) - .jwsKeySelector(jwkSource) - .block(); + .jwsKeySelector(jwkSource); assertThat(jwsKeySelector instanceof JWSVerificationKeySelector); JWSVerificationKeySelector jwsVerificationKeySelector = (JWSVerificationKeySelector) jwsKeySelector; assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS256)).isTrue(); @@ -633,11 +613,10 @@ public class NimbusReactiveJwtDecoderTests { @Test public void jwsKeySelectorWhenOneAlgorithmThenReturnsSingleSelector() { - ReactiveRemoteJWKSource jwkSource = mock(ReactiveRemoteJWKSource.class); + JWKSource jwkSource = mock(JWKSource.class); JWSKeySelector jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithm(SignatureAlgorithm.RS512) - .jwsKeySelector(jwkSource) - .block(); + .jwsKeySelector(jwkSource); assertThat(jwsKeySelector instanceof JWSVerificationKeySelector); JWSVerificationKeySelector jwsVerificationKeySelector = (JWSVerificationKeySelector) jwsKeySelector; assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS512)).isTrue(); @@ -645,12 +624,12 @@ public class NimbusReactiveJwtDecoderTests { @Test public void jwsKeySelectorWhenMultipleAlgorithmThenReturnsCompositeSelector() { - ReactiveRemoteJWKSource jwkSource = mock(ReactiveRemoteJWKSource.class); + JWKSource jwkSource = mock(JWKSource.class); // @formatter:off JWSKeySelector jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) .jwsAlgorithm(SignatureAlgorithm.RS256) .jwsAlgorithm(SignatureAlgorithm.RS512) - .jwsKeySelector(jwkSource).block(); + .jwsKeySelector(jwkSource); // @formatter:on assertThat(jwsKeySelector instanceof JWSVerificationKeySelector); JWSVerificationKeySelector jwsAlgorithmMapKeySelector = (JWSVerificationKeySelector) jwsKeySelector; diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java deleted file mode 100644 index 30a0affd14..0000000000 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright 2002-2023 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.jwt; - -import java.net.URI; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import okhttp3.HttpUrl; -import okhttp3.mockwebserver.Dispatcher; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.util.UriComponentsBuilder; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - -/** - * Tests for {@link ReactiveJwtDecoderProviderConfigurationUtils} - * - * @author Josh Cummings - */ -public class ReactiveJwtDecoderProviderConfigurationUtilsTests { - - /** - * Contains those parameters required to construct a ReactiveJwtDecoder as well as any - * required parameters - */ - // @formatter:off - private static final String DEFAULT_RESPONSE_TEMPLATE = "{\n" - + " \"authorization_endpoint\": \"https://example.com/o/oauth2/v2/auth\", \n" - + " \"id_token_signing_alg_values_supported\": [\n" - + " \"RS256\"\n" - + " ], \n" - + " \"issuer\": \"%s\", \n" - + " \"jwks_uri\": \"%s/.well-known/jwks.json\", \n" - + " \"response_types_supported\": [\n" - + " \"code\", \n" - + " \"token\", \n" - + " \"id_token\", \n" - + " \"code token\", \n" - + " \"code id_token\", \n" - + " \"token id_token\", \n" - + " \"code token id_token\", \n" - + " \"none\"\n" - + " ], \n" - + " \"subject_types_supported\": [\n" - + " \"public\"\n" - + " ], \n" - + " \"token_endpoint\": \"https://example.com/oauth2/v4/token\"\n" - + "}"; - // @formatter:on - - private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; - - private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration"; - - private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server"; - - private final WebClient web = WebClient.builder().build(); - - private MockWebServer server; - - private String issuer; - - @BeforeEach - public void setup() throws Exception { - this.server = new MockWebServer(); - this.server.start(); - this.issuer = createIssuerFromServer(); - this.issuer += "path"; - } - - @AfterEach - public void cleanup() throws Exception { - this.server.shutdown(); - } - - @Test - public void issuerWhenResponseIsTypicalThenReturnedConfigurationContainsJwksUri() { - prepareConfigurationResponse(); - Map configuration = ReactiveJwtDecoderProviderConfigurationUtils - .getConfigurationForIssuerLocation(this.issuer, this.web) - .block(); - assertThat(configuration).containsKey("jwks_uri"); - } - - @Test - public void issuerWhenOidcFallbackResponseIsTypicalThenReturnedConfigurationContainsJwksUri() { - prepareConfigurationResponseOidc(); - Map configuration = ReactiveJwtDecoderProviderConfigurationUtils - .getConfigurationForIssuerLocation(this.issuer, this.web) - .block(); - assertThat(configuration).containsKey("jwks_uri"); - } - - @Test - public void issuerWhenOAuth2ResponseIsTypicalThenReturnedConfigurationContainsJwksUri() { - prepareConfigurationResponseOAuth2(); - Map configuration = ReactiveJwtDecoderProviderConfigurationUtils - .getConfigurationForIssuerLocation(this.issuer, this.web) - .block(); - assertThat(configuration).containsKey("jwks_uri"); - } - - @Test - public void issuerWhenOidcFallbackResponseIsNonCompliantThenThrowsRuntimeException() { - prepareConfigurationResponseOidc("{ \"missing_required_keys\" : \"and_values\" }"); - // @formatter:off - assertThatExceptionOfType(RuntimeException.class) - .isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block()); - // @formatter:on - } - - @Test - public void issuerWhenOAuth2ResponseIsNonCompliantThenThrowsRuntimeException() { - prepareConfigurationResponseOAuth2("{ \"missing_required_keys\" : \"and_values\" }"); - // @formatter:off - assertThatExceptionOfType(RuntimeException.class) - .isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block()); - // @formatter:on - } - - // gh-7512 - @Test - public void issuerWhenOidcFallbackResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() - throws JsonMappingException, JsonProcessingException { - prepareConfigurationResponseOidc(this.buildResponseWithMissingJwksUri()); - // @formatter:off - assertThatIllegalArgumentException() - .isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block()) - .withMessage("The public JWK set URI must not be null"); - // @formatter:on - } - - // gh-7512 - @Test - public void issuerWhenOAuth2ResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() - throws JsonMappingException, JsonProcessingException { - prepareConfigurationResponseOAuth2(this.buildResponseWithMissingJwksUri()); - // @formatter:off - assertThatIllegalArgumentException() - .isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block()) - .withMessage("The public JWK set URI must not be null"); - // @formatter:on - } - - @Test - public void issuerWhenOidcFallbackResponseIsMalformedThenThrowsRuntimeException() { - prepareConfigurationResponseOidc("malformed"); - // @formatter:off - assertThatExceptionOfType(RuntimeException.class) - .isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block()); - // @formatter:on - } - - @Test - public void issuerWhenOAuth2ResponseIsMalformedThenThrowsRuntimeException() { - prepareConfigurationResponseOAuth2("malformed"); - // @formatter:off - assertThatExceptionOfType(RuntimeException.class) - .isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block()); - // @formatter:on - } - - @Test - public void issuerWhenOidcFallbackRespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() { - prepareConfigurationResponseOidc(String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer + "/wrong", this.issuer)); - // @formatter:off - assertThatIllegalStateException() - .isThrownBy(() -> { - Map configuration = ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block(); - JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, this.issuer); - }); - // @formatter:on - } - - @Test - public void issuerWhenOAuth2RespondingIssuerMismatchesRequestedIssuerThenThrowsIllegalStateException() { - prepareConfigurationResponseOAuth2( - String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer + "/wrong", this.issuer)); - // @formatter:off - assertThatIllegalStateException() - .isThrownBy(() -> { - Map configuration = ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(this.issuer, this.web).block(); - JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, this.issuer); - }); - // @formatter:on - } - - @Test - public void issuerWhenOidcFallbackRequestedIssuerIsUnresponsiveThenThrowsIllegalArgumentException() - throws Exception { - this.server.shutdown(); - // @formatter:off - assertThatIllegalArgumentException() - .isThrownBy(() -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation("https://issuer", this.web).block()); - // @formatter:on - } - - private void prepareConfigurationResponse() { - String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer); - prepareConfigurationResponse(body); - } - - private void prepareConfigurationResponse(String body) { - this.server.enqueue(response(body)); - this.server.enqueue(response(JWK_SET)); - } - - private void prepareConfigurationResponseOidc() { - String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer); - prepareConfigurationResponseOidc(body); - } - - private void prepareConfigurationResponseOidc(String body) { - Map responses = new HashMap<>(); - responses.put(oidc(), response(body)); - responses.put(jwks(), response(JWK_SET)); - prepareConfigurationResponses(responses); - } - - private void prepareConfigurationResponseOAuth2() { - String body = String.format(DEFAULT_RESPONSE_TEMPLATE, this.issuer, this.issuer); - prepareConfigurationResponseOAuth2(body); - } - - private void prepareConfigurationResponseOAuth2(String body) { - Map responses = new HashMap<>(); - responses.put(oauth(), response(body)); - responses.put(jwks(), response(JWK_SET)); - prepareConfigurationResponses(responses); - } - - private void prepareConfigurationResponses(Map responses) { - Dispatcher dispatcher = new Dispatcher() { - @Override - public MockResponse dispatch(RecordedRequest request) { - // @formatter:off - return Optional.of(request) - .map(RecordedRequest::getRequestUrl) - .map(HttpUrl::toString) - .map(responses::get) - .orElse(new MockResponse().setResponseCode(404)); - // @formatter:on - } - }; - this.server.setDispatcher(dispatcher); - } - - private String createIssuerFromServer() { - return this.server.url("").toString(); - } - - private String oidc() { - URI uri = URI.create(this.issuer); - // @formatter:off - return UriComponentsBuilder.fromUri(uri) - .replacePath(uri.getPath() + OIDC_METADATA_PATH) - .toUriString(); - // @formatter:on - } - - private String oauth() { - URI uri = URI.create(this.issuer); - // @formatter:off - return UriComponentsBuilder.fromUri(uri) - .replacePath(OAUTH_METADATA_PATH + uri.getPath()) - .toUriString(); - // @formatter:on - } - - private String jwks() { - return this.issuer + "/.well-known/jwks.json"; - } - - private MockResponse response(String body) { - // @formatter:off - return new MockResponse().setBody(body) - .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); - // @formatter:on - } - - public String buildResponseWithMissingJwksUri() throws JsonMappingException, JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); - Map response = mapper.readValue(DEFAULT_RESPONSE_TEMPLATE, - new TypeReference>() { - }); - response.remove("jwks_uri"); - return mapper.writeValueAsString(response); - } - -} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java index cdca4ffc0c..000457b6f8 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 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. @@ -56,7 +56,6 @@ import org.springframework.util.Assert; * * @author Josh Cummings * @author Joe Grandja - * @author Jerome Wacongne ch4mp@c4-soft.com * @since 5.1 * @see AuthenticationProvider * @see JwtDecoder @@ -87,9 +86,7 @@ public final class JwtAuthenticationProvider implements AuthenticationProvider { BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication; Jwt jwt = getJwt(bearer); AbstractAuthenticationToken token = this.jwtAuthenticationConverter.convert(jwt); - if (token.getDetails() == null) { - token.setDetails(bearer.getDetails()); - } + token.setDetails(bearer.getDetails()); this.logger.debug("Authenticated token"); return token; } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverter.java index bc625793ef..ab020a3fc8 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * 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. @@ -45,14 +45,10 @@ public final class JwtGrantedAuthoritiesConverter implements Converter WELL_KNOWN_AUTHORITIES_CLAIM_NAMES = Arrays.asList("scope", "scp"); private String authorityPrefix = DEFAULT_AUTHORITY_PREFIX; - private String authoritiesClaimDelimiter = DEFAULT_AUTHORITIES_CLAIM_DELIMITER; - private String authoritiesClaimName; /** @@ -81,18 +77,6 @@ public final class JwtGrantedAuthoritiesConverter implements ConverterIssuer in * a signed JWT (JWS). * - * To use, this class must be able to determine whether the `iss` claim is trusted. Recall - * that anyone can stand up an authorization server and issue valid tokens to a resource - * server. The simplest way to achieve this is to supply a set of trusted issuers in the - * constructor. + * To use, this class must be able to determine whether or not the `iss` claim is trusted. + * Recall that anyone can stand up an authorization server and issue valid tokens to a + * resource server. The simplest way to achieve this is to supply a list of trusted + * issuers in the constructor. * * This class derives the Issuer from the `iss` claim found in the * {@link HttpServletRequest}'s @@ -66,58 +67,22 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat /** * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided * parameters - * @param trustedIssuers an array of trusted issuers - * @deprecated use {@link #fromTrustedIssuers(String...)} + * @param trustedIssuers a list of trusted issuers */ - @Deprecated(since = "6.2", forRemoval = true) public JwtIssuerAuthenticationManagerResolver(String... trustedIssuers) { - this(Set.of(trustedIssuers)); + this(Arrays.asList(trustedIssuers)); } /** * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided * parameters - * @param trustedIssuers a collection of trusted issuers - * @deprecated use {@link #fromTrustedIssuers(Collection)} + * @param trustedIssuers a list of trusted issuers */ - @Deprecated(since = "6.2", forRemoval = true) public JwtIssuerAuthenticationManagerResolver(Collection trustedIssuers) { Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty"); this.authenticationManager = new ResolvingAuthenticationManager( - new TrustedIssuerJwtAuthenticationManagerResolver(Set.copyOf(trustedIssuers)::contains)); - } - - /** - * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided - * parameters - * @param trustedIssuers an array of trusted issuers - * @since 6.2 - */ - public static JwtIssuerAuthenticationManagerResolver fromTrustedIssuers(String... trustedIssuers) { - return fromTrustedIssuers(Set.of(trustedIssuers)); - } - - /** - * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided - * parameters - * @param trustedIssuers a collection of trusted issuers - * @since 6.2 - */ - public static JwtIssuerAuthenticationManagerResolver fromTrustedIssuers(Collection trustedIssuers) { - Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty"); - return fromTrustedIssuers(Set.copyOf(trustedIssuers)::contains); - } - - /** - * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided - * parameters - * @param trustedIssuers a predicate to validate issuers - * @since 6.2 - */ - public static JwtIssuerAuthenticationManagerResolver fromTrustedIssuers(Predicate trustedIssuers) { - Assert.notNull(trustedIssuers, "trustedIssuers cannot be null"); - return new JwtIssuerAuthenticationManagerResolver( - new TrustedIssuerJwtAuthenticationManagerResolver(trustedIssuers)); + new TrustedIssuerJwtAuthenticationManagerResolver( + Collections.unmodifiableCollection(trustedIssuers)::contains)); } /** @@ -125,8 +90,8 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat * parameters * * Note that the {@link AuthenticationManagerResolver} provided in this constructor - * will need to verify that the issuer is trusted. This should be done via an allowed - * set of issuers. + * will need to verify that the issuer is trusted. This should be done via an + * allowlist. * * One way to achieve this is with a {@link Map} where the keys are the known issuers: *
      diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java
      index 2e81d3b3d8..f31949d937 100644
      --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java
      +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java
      @@ -17,20 +17,18 @@
       package org.springframework.security.oauth2.server.resource.authentication;
       
       import java.time.Duration;
      +import java.util.ArrayList;
      +import java.util.Arrays;
       import java.util.Collection;
       import java.util.Map;
      -import java.util.Set;
       import java.util.concurrent.ConcurrentHashMap;
       import java.util.function.Predicate;
       
       import com.nimbusds.jwt.JWTParser;
      -import org.apache.commons.logging.Log;
      -import org.apache.commons.logging.LogFactory;
       import reactor.core.publisher.Mono;
       import reactor.core.scheduler.Schedulers;
       
       import org.springframework.core.convert.converter.Converter;
      -import org.springframework.core.log.LogMessage;
       import org.springframework.lang.NonNull;
       import org.springframework.security.authentication.AuthenticationManager;
       import org.springframework.security.authentication.ReactiveAuthenticationManager;
      @@ -48,10 +46,10 @@ import org.springframework.web.server.ServerWebExchange;
        * "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer in
        * a signed JWT (JWS).
        *
      - * To use, this class must be able to determine whether the `iss` claim is trusted. Recall
      - * that anyone can stand up an authorization server and issue valid tokens to a resource
      - * server. The simplest way to achieve this is to supply a set of trusted issuers in the
      - * constructor.
      + * To use, this class must be able to determine whether or not the `iss` claim is trusted.
      + * Recall that anyone can stand up an authorization server and issue valid tokens to a
      + * resource server. The simplest way to achieve this is to supply a list of trusted
      + * issuers in the constructor.
        *
        * This class derives the Issuer from the `iss` claim found in the
        * {@link ServerWebExchange}'s
      @@ -70,58 +68,21 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver
       	/**
       	 * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
       	 * provided parameters
      -	 * @param trustedIssuers an array of trusted issuers
      -	 * @deprecated use {@link #fromTrustedIssuers(String...)}
      +	 * @param trustedIssuers a list of trusted issuers
       	 */
      -	@Deprecated(since = "6.2", forRemoval = true)
       	public JwtIssuerReactiveAuthenticationManagerResolver(String... trustedIssuers) {
      -		this(Set.of(trustedIssuers));
      +		this(Arrays.asList(trustedIssuers));
       	}
       
       	/**
       	 * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
       	 * provided parameters
       	 * @param trustedIssuers a collection of trusted issuers
      -	 * @deprecated use {@link #fromTrustedIssuers(Collection)}
       	 */
      -	@Deprecated(since = "6.2", forRemoval = true)
       	public JwtIssuerReactiveAuthenticationManagerResolver(Collection trustedIssuers) {
       		Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
       		this.authenticationManager = new ResolvingAuthenticationManager(
      -				new TrustedIssuerJwtAuthenticationManagerResolver(Set.copyOf(trustedIssuers)::contains));
      -	}
      -
      -	/**
      -	 * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
      -	 * provided parameters
      -	 * @param trustedIssuers an array of trusted issuers
      -	 * @since 6.2
      -	 */
      -	public static JwtIssuerReactiveAuthenticationManagerResolver fromTrustedIssuers(String... trustedIssuers) {
      -		return fromTrustedIssuers(Set.of(trustedIssuers));
      -	}
      -
      -	/**
      -	 * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
      -	 * provided parameters
      -	 * @param trustedIssuers a collection of trusted issuers
      -	 * @since 6.2
      -	 */
      -	public static JwtIssuerReactiveAuthenticationManagerResolver fromTrustedIssuers(Collection trustedIssuers) {
      -		Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
      -		return fromTrustedIssuers(Set.copyOf(trustedIssuers)::contains);
      -	}
      -
      -	/**
      -	 * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
      -	 * provided parameters
      -	 * @param trustedIssuers a predicate to validate issuers
      -	 * @since 6.2
      -	 */
      -	public static JwtIssuerReactiveAuthenticationManagerResolver fromTrustedIssuers(Predicate trustedIssuers) {
      -		Assert.notNull(trustedIssuers, "trustedIssuers cannot be null");
      -		return new JwtIssuerReactiveAuthenticationManagerResolver(
      -				new TrustedIssuerJwtAuthenticationManagerResolver(trustedIssuers));
      +				new TrustedIssuerJwtAuthenticationManagerResolver(new ArrayList<>(trustedIssuers)::contains));
       	}
       
       	/**
      @@ -130,7 +91,7 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver
       	 *
       	 * Note that the {@link ReactiveAuthenticationManagerResolver} provided in this
       	 * constructor will need to verify that the issuer is trusted. This should be done via
      -	 * an allowed set of issuers.
      +	 * an allowed list of issuers.
       	 *
       	 * One way to achieve this is with a {@link Map} where the keys are the known issuers:
       	 * 
      @@ -208,8 +169,6 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver
       	static class TrustedIssuerJwtAuthenticationManagerResolver
       			implements ReactiveAuthenticationManagerResolver {
       
      -		private final Log logger = LogFactory.getLog(getClass());
      -
       		private final Map> authenticationManagers = new ConcurrentHashMap<>();
       
       		private final Predicate trustedIssuer;
      @@ -221,13 +180,11 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver
       		@Override
       		public Mono resolve(String issuer) {
       			if (!this.trustedIssuer.test(issuer)) {
      -				this.logger.debug("Did not resolve AuthenticationManager since issuer is not trusted");
       				return Mono.empty();
       			}
       			// @formatter:off
       			return this.authenticationManagers.computeIfAbsent(issuer,
       					(k) -> Mono.fromCallable(() -> new JwtReactiveAuthenticationManager(ReactiveJwtDecoders.fromIssuerLocation(k)))
      -							.doOnNext((manager) -> this.logger.debug(LogMessage.format("Resolved AuthenticationManager for issuer '%s'", issuer)))
       							.subscribeOn(Schedulers.boundedElastic())
       							.cache((manager) -> Duration.ofMillis(Long.MAX_VALUE), (ex) -> Duration.ZERO, () -> Duration.ZERO)
       			);
      diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java
      index 96d862c102..eb1d63ff8f 100644
      --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java
      +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java
      @@ -101,9 +101,10 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
       	 */
       	@Override
       	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      -		if (!(authentication instanceof BearerTokenAuthenticationToken bearer)) {
      +		if (!(authentication instanceof BearerTokenAuthenticationToken)) {
       			return null;
       		}
      +		BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
       		OAuth2AuthenticatedPrincipal principal = getOAuth2AuthenticatedPrincipal(bearer);
       		Authentication result = this.authenticationConverter.convert(bearer.getToken(), principal);
       		if (result == null) {
      diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java
      index c80fce5797..05e2f93a13 100644
      --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java
      +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java
      @@ -1,5 +1,5 @@
       /*
      - * Copyright 2002-2023 the original author or authors.
      + * 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.
      @@ -23,7 +23,6 @@ import org.springframework.core.convert.converter.Converter;
       import org.springframework.security.authentication.AbstractAuthenticationToken;
       import org.springframework.security.core.GrantedAuthority;
       import org.springframework.security.oauth2.jwt.Jwt;
      -import org.springframework.security.oauth2.jwt.JwtClaimNames;
       import org.springframework.util.Assert;
       
       /**
      @@ -31,7 +30,6 @@ import org.springframework.util.Assert;
        * a {@link AbstractAuthenticationToken Mono<AbstractAuthenticationToken>}.
        *
        * @author Eric Deandrea
      - * @author Marcus Kainth
        * @since 5.2
        */
       public final class ReactiveJwtAuthenticationConverter implements Converter> {
      @@ -39,17 +37,12 @@ public final class ReactiveJwtAuthenticationConverter implements Converter> jwtGrantedAuthoritiesConverter = new ReactiveJwtGrantedAuthoritiesConverterAdapter(
       			new JwtGrantedAuthoritiesConverter());
       
      -	private String principalClaimName = JwtClaimNames.SUB;
      -
       	@Override
       	public Mono convert(Jwt jwt) {
       		// @formatter:off
       		return this.jwtGrantedAuthoritiesConverter.convert(jwt)
       				.collectList()
      -				.map((authorities) -> {
      -					String principalName = jwt.getClaimAsString(this.principalClaimName);
      -					return new JwtAuthenticationToken(jwt, authorities, principalName);
      -				});
      +				.map((authorities) -> new JwtAuthenticationToken(jwt, authorities));
       		// @formatter:on
       	}
       
      @@ -65,14 +58,4 @@ public final class ReactiveJwtAuthenticationConverter implements Converter[a-zA-Z0-9-._~+/]+=*)$",
       			Pattern.CASE_INSENSITIVE);
       
      @@ -118,7 +115,7 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
       	}
       
       	private static String resolveFromRequestParameters(HttpServletRequest request) {
      -		String[] values = request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME);
      +		String[] values = request.getParameterValues("access_token");
       		if (values == null || values.length == 0) {
       			return null;
       		}
      @@ -130,24 +127,15 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
       	}
       
       	private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) {
      -		return isFormEncodedRequest(request) || isGetRequest(request);
      +		return (("POST".equals(request.getMethod())
      +				&& MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()))
      +				|| "GET".equals(request.getMethod()));
       	}
       
      -	private static boolean isGetRequest(HttpServletRequest request) {
      -		return HttpMethod.GET.name().equals(request.getMethod());
      -	}
      -
      -	private static boolean isFormEncodedRequest(HttpServletRequest request) {
      -		return MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType());
      -	}
      -
      -	private static boolean hasAccessTokenInQueryString(HttpServletRequest request) {
      -		return (request.getQueryString() != null) && request.getQueryString().contains(ACCESS_TOKEN_PARAMETER_NAME);
      -	}
      -
      -	private boolean isParameterTokenEnabledForRequest(HttpServletRequest request) {
      -		return ((this.allowFormEncodedBodyParameter && isFormEncodedRequest(request) && !isGetRequest(request)
      -				&& !hasAccessTokenInQueryString(request)) || (this.allowUriQueryParameter && isGetRequest(request)));
      +	private boolean isParameterTokenEnabledForRequest(final HttpServletRequest request) {
      +		return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod())
      +				&& MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()))
      +				|| (this.allowUriQueryParameter && "GET".equals(request.getMethod())));
       	}
       
       }
      diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java
      index a2bc58a50d..7c7ee2aa44 100644
      --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java
      +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java
      @@ -82,7 +82,8 @@ public final class BearerTokenServerAuthenticationEntryPoint implements ServerAu
       			if (StringUtils.hasText(error.getUri())) {
       				parameters.put("error_uri", error.getUri());
       			}
      -			if (error instanceof BearerTokenError bearerTokenError) {
      +			if (error instanceof BearerTokenError) {
      +				BearerTokenError bearerTokenError = (BearerTokenError) error;
       				if (StringUtils.hasText(bearerTokenError.getScope())) {
       					parameters.put("scope", bearerTokenError.getScope());
       				}
      diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java
      index b4438ba28f..5345736138 100644
      --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java
      +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java
      @@ -1,5 +1,5 @@
       /*
      - * Copyright 2002-2022 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,7 +25,6 @@ import org.mockito.Mock;
       import org.mockito.junit.jupiter.MockitoExtension;
       
       import org.springframework.core.convert.converter.Converter;
      -import org.springframework.security.authentication.AbstractAuthenticationToken;
       import org.springframework.security.core.AuthenticationException;
       import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
       import org.springframework.security.oauth2.jwt.BadJwtException;
      @@ -44,13 +43,12 @@ import static org.mockito.Mockito.mock;
        * Tests for {@link JwtAuthenticationProvider}
        *
        * @author Josh Cummings
      - * @author Jerome Wacongne ch4mp@c4-soft.com
        */
       @ExtendWith(MockitoExtension.class)
       public class JwtAuthenticationProviderTests {
       
       	@Mock
      -	Converter jwtAuthenticationConverter;
      +	Converter jwtAuthenticationConverter;
       
       	@Mock
       	JwtDecoder jwtDecoder;
      @@ -109,17 +107,6 @@ public class JwtAuthenticationProviderTests {
       
       	@Test
       	public void authenticateWhenConverterReturnsAuthenticationThenProviderPropagatesIt() {
      -		BearerTokenAuthenticationToken token = this.authentication();
      -		Jwt jwt = TestJwts.jwt().build();
      -		JwtAuthenticationToken authentication = new JwtAuthenticationToken(jwt);
      -		given(this.jwtDecoder.decode(token.getToken())).willReturn(jwt);
      -		given(this.jwtAuthenticationConverter.convert(jwt)).willReturn(authentication);
      -
      -		assertThat(this.provider.authenticate(token)).isEqualTo(authentication);
      -	}
      -
      -	@Test
      -	public void authenticateWhenConverterDoesNotSetAuthenticationDetailsThenProviderSetsItWithTokenDetails() {
       		BearerTokenAuthenticationToken token = this.authentication();
       		Object details = mock(Object.class);
       		token.setDetails(details);
      @@ -130,25 +117,7 @@ public class JwtAuthenticationProviderTests {
       		// @formatter:off
       		assertThat(this.provider.authenticate(token))
       				.isEqualTo(authentication).hasFieldOrPropertyWithValue("details",
      -						details);
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void authenticateWhenConverterSetsAuthenticationDetailsThenProviderDoesNotOverwriteIt() {
      -		BearerTokenAuthenticationToken token = this.authentication();
      -		Object details = mock(Object.class);
      -		token.setDetails(details);
      -		Jwt jwt = TestJwts.jwt().build();
      -		JwtAuthenticationToken authentication = new JwtAuthenticationToken(jwt);
      -		Object expectedDetails = "To be kept as is";
      -		authentication.setDetails(expectedDetails);
      -		given(this.jwtDecoder.decode(token.getToken())).willReturn(jwt);
      -		given(this.jwtAuthenticationConverter.convert(jwt)).willReturn(authentication);
      -		// @formatter:off
      -		assertThat(this.provider.authenticate(token))
      -				.isEqualTo(authentication).hasFieldOrPropertyWithValue("details",
      -						expectedDetails);
      +				details);
       		// @formatter:on
       	}
       
      diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverterTests.java
      index 2cb5390907..ff3cc30b8a 100644
      --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverterTests.java
      +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverterTests.java
      @@ -1,5 +1,5 @@
       /*
      - * Copyright 2002-2022 the original author or authors.
      + * 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.
      @@ -256,18 +256,4 @@ public class JwtGrantedAuthoritiesConverterTests {
       		assertThat(authorities).isEmpty();
       	}
       
      -	@Test
      -	public void convertWithCustomAuthoritiesSplitRegexWhenTokenHasScopeAttributeThenTranslatedToAuthorities() {
      -		// @formatter:off
      -		Jwt jwt = TestJwts.jwt()
      -				.claim("scope", "message:read,message:write")
      -				.build();
      -		// @formatter:on
      -		JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
      -		jwtGrantedAuthoritiesConverter.setAuthoritiesClaimDelimiter(",");
      -		Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
      -		assertThat(authorities).containsExactly(new SimpleGrantedAuthority("SCOPE_message:read"),
      -				new SimpleGrantedAuthority("SCOPE_message:write"));
      -	}
      -
       }
      diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverDeprecatedTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverDeprecatedTests.java
      deleted file mode 100644
      index 9301ed104b..0000000000
      --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverDeprecatedTests.java
      +++ /dev/null
      @@ -1,257 +0,0 @@
      -/*
      - * Copyright 2002-2020 the original author or authors.
      - *
      - * Licensed under the Apache License, Version 2.0 (the "License");
      - * you may not use this file except in compliance with the License.
      - * You may obtain a copy of the License at
      - *
      - *      https://www.apache.org/licenses/LICENSE-2.0
      - *
      - * Unless required by applicable law or agreed to in writing, software
      - * distributed under the License is distributed on an "AS IS" BASIS,
      - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      - * See the License for the specific language governing permissions and
      - * limitations under the License.
      - */
      -
      -package org.springframework.security.oauth2.server.resource.authentication;
      -
      -import java.util.Collection;
      -import java.util.Collections;
      -import java.util.HashMap;
      -import java.util.Map;
      -
      -import com.nimbusds.jose.JWSAlgorithm;
      -import com.nimbusds.jose.JWSHeader;
      -import com.nimbusds.jose.JWSObject;
      -import com.nimbusds.jose.Payload;
      -import com.nimbusds.jose.crypto.RSASSASigner;
      -import com.nimbusds.jwt.JWTClaimsSet;
      -import com.nimbusds.jwt.PlainJWT;
      -import net.minidev.json.JSONObject;
      -import okhttp3.mockwebserver.MockResponse;
      -import okhttp3.mockwebserver.MockWebServer;
      -import org.junit.jupiter.api.Test;
      -
      -import org.springframework.security.authentication.AuthenticationManager;
      -import org.springframework.security.authentication.AuthenticationManagerResolver;
      -import org.springframework.security.core.Authentication;
      -import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
      -import org.springframework.security.oauth2.jose.TestKeys;
      -import org.springframework.security.oauth2.jwt.JwtClaimNames;
      -import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver;
      -
      -import static org.assertj.core.api.Assertions.assertThat;
      -import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
      -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
      -import static org.mockito.BDDMockito.mock;
      -import static org.mockito.BDDMockito.verify;
      -
      -/**
      - * Tests for {@link JwtIssuerAuthenticationManagerResolver}
      - */
      -@Deprecated
      -public class JwtIssuerAuthenticationManagerResolverDeprecatedTests {
      -
      -	private static final String DEFAULT_RESPONSE_TEMPLATE = "{\n" + "    \"issuer\": \"%s\", \n"
      -			+ "    \"jwks_uri\": \"%s/.well-known/jwks.json\" \n" + "}";
      -
      -	private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"3FlqJr5TRskIQIgdE3Dd7D9lboWdcTUT8a-fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRvc5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4_1tfRgG6ii4Uhxh6iI8qNMJQX-fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2kJdJ_ZIV-WW4noDdzpKqHcwmB8FsrumlVY_DNVvUSDIipiq9PbP4H99TXN1o746oRaNa07rq1hoCgMSSy-85SagCoxlmyE-D-of9SsMY8Ol9t0rdzpobBuhyJ_o5dfvjKw\"}]}";
      -
      -	private String jwt = jwt("iss", "trusted");
      -
      -	private String evil = jwt("iss", "\"");
      -
      -	private String noIssuer = jwt("sub", "sub");
      -
      -	@Test
      -	public void resolveWhenUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception {
      -		try (MockWebServer server = new MockWebServer()) {
      -			server.start();
      -			String issuer = server.url("").toString();
      -			// @formatter:off
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -					.setHeader("Content-Type", "application/json")
      -					.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)
      -			));
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -					.setHeader("Content-Type", "application/json")
      -					.setBody(JWK_SET)
      -			);
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -					.setHeader("Content-Type", "application/json")
      -					.setBody(JWK_SET)
      -			);
      -			// @formatter:on
      -			JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
      -					new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer))));
      -			jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
      -			JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      -					issuer);
      -			Authentication token = withBearerToken(jws.serialize());
      -			AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null);
      -			assertThat(authenticationManager).isNotNull();
      -			Authentication authentication = authenticationManager.authenticate(token);
      -			assertThat(authentication.isAuthenticated()).isTrue();
      -		}
      -	}
      -
      -	@Test
      -	public void resolveWhednUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception {
      -		try (MockWebServer server = new MockWebServer()) {
      -			server.start();
      -			String issuer = server.url("").toString();
      -			// @formatter:off
      -			server.enqueue(new MockResponse().setResponseCode(500)
      -					.setHeader("Content-Type", "application/json")
      -					.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer))
      -			);
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -					.setHeader("Content-Type", "application/json")
      -					.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer))
      -			);
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -					.setHeader("Content-Type", "application/json")
      -					.setBody(JWK_SET)
      -			);
      -			// @formatter:on
      -			JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
      -					new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer))));
      -			jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
      -			JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      -					issuer);
      -			Authentication token = withBearerToken(jws.serialize());
      -			AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null);
      -			assertThat(authenticationManager).isNotNull();
      -			assertThatExceptionOfType(IllegalArgumentException.class)
      -				.isThrownBy(() -> authenticationManager.authenticate(token));
      -			Authentication authentication = authenticationManager.authenticate(token);
      -			assertThat(authentication.isAuthenticated()).isTrue();
      -		}
      -	}
      -
      -	@Test
      -	public void resolveWhenUsingSameIssuerThenReturnsSameAuthenticationManager() throws Exception {
      -		try (MockWebServer server = new MockWebServer()) {
      -			String issuer = server.url("").toString();
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -				.setHeader("Content-Type", "application/json")
      -				.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)));
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -				.setHeader("Content-Type", "application/json")
      -				.setBody(JWK_SET));
      -			TrustedIssuerJwtAuthenticationManagerResolver resolver = new TrustedIssuerJwtAuthenticationManagerResolver(
      -					(iss) -> iss.equals(issuer));
      -			AuthenticationManager authenticationManager = resolver.resolve(issuer);
      -			AuthenticationManager cachedAuthenticationManager = resolver.resolve(issuer);
      -			assertThat(authenticationManager).isSameAs(cachedAuthenticationManager);
      -		}
      -	}
      -
      -	@Test
      -	public void resolveWhenUsingUntrustedIssuerThenException() {
      -		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      -				"other", "issuers");
      -		Authentication token = withBearerToken(this.jwt);
      -		// @formatter:off
      -		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      -				.isThrownBy(() -> authenticationManagerResolver.resolve(null).authenticate(token))
      -				.withMessageContaining("Invalid issuer");
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverThenUses() {
      -		AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
      -		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      -				(issuer) -> authenticationManager);
      -		Authentication token = withBearerToken(this.jwt);
      -		authenticationManagerResolver.resolve(null).authenticate(token);
      -		verify(authenticationManager).authenticate(token);
      -	}
      -
      -	@Test
      -	public void resolveWhenUsingExternalSourceThenRespondsToChanges() {
      -		Authentication token = withBearerToken(this.jwt);
      -		Map authenticationManagers = new HashMap<>();
      -		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      -				authenticationManagers::get);
      -		// @formatter:off
      -		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      -				.isThrownBy(() -> authenticationManagerResolver.resolve(null).authenticate(token))
      -				.withMessageContaining("Invalid issuer");
      -		// @formatter:on
      -		AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
      -		authenticationManagers.put("trusted", authenticationManager);
      -		authenticationManagerResolver.resolve(null).authenticate(token);
      -		verify(authenticationManager).authenticate(token);
      -		authenticationManagers.clear();
      -		// @formatter:off
      -		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      -				.isThrownBy(() -> authenticationManagerResolver.resolve(null).authenticate(token))
      -				.withMessageContaining("Invalid issuer");
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void resolveWhenBearerTokenMalformedThenException() {
      -		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      -				"trusted");
      -		Authentication token = withBearerToken("jwt");
      -		// @formatter:off
      -		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      -				.isThrownBy(() -> authenticationManagerResolver.resolve(null).authenticate(token))
      -				.withMessageNotContaining("Invalid issuer");
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void resolveWhenBearerTokenNoIssuerThenException() {
      -		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      -				"trusted");
      -		Authentication token = withBearerToken(this.noIssuer);
      -		// @formatter:off
      -		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      -				.isThrownBy(() -> authenticationManagerResolver.resolve(null).authenticate(token))
      -				.withMessageContaining("Missing issuer");
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void resolveWhenBearerTokenEvilThenGenericException() {
      -		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      -				"trusted");
      -		Authentication token = withBearerToken(this.evil);
      -		// @formatter:off
      -		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      -				.isThrownBy(() -> authenticationManagerResolver
      -						.resolve(null).authenticate(token)
      -				)
      -				.withMessage("Invalid issuer");
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void constructorWhenNullOrEmptyIssuersThenException() {
      -		assertThatIllegalArgumentException()
      -			.isThrownBy(() -> new JwtIssuerAuthenticationManagerResolver((Collection) null));
      -		assertThatIllegalArgumentException()
      -			.isThrownBy(() -> new JwtIssuerAuthenticationManagerResolver(Collections.emptyList()));
      -	}
      -
      -	@Test
      -	public void constructorWhenNullAuthenticationManagerResolverThenException() {
      -		assertThatIllegalArgumentException()
      -			.isThrownBy(() -> new JwtIssuerAuthenticationManagerResolver((AuthenticationManagerResolver) null));
      -	}
      -
      -	private Authentication withBearerToken(String token) {
      -		return new BearerTokenAuthenticationToken(token);
      -	}
      -
      -	private String jwt(String claim, String value) {
      -		PlainJWT jwt = new PlainJWT(new JWTClaimsSet.Builder().claim(claim, value).build());
      -		return jwt.serialize();
      -	}
      -
      -}
      diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java
      index 8a50dab153..fc9fed19b8 100644
      --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java
      +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java
      @@ -20,7 +20,6 @@ import java.util.Collection;
       import java.util.Collections;
       import java.util.HashMap;
       import java.util.Map;
      -import java.util.function.Predicate;
       
       import com.nimbusds.jose.JWSAlgorithm;
       import com.nimbusds.jose.JWSHeader;
      @@ -65,7 +64,7 @@ public class JwtIssuerAuthenticationManagerResolverTests {
       	private String noIssuer = jwt("sub", "sub");
       
       	@Test
      -	public void resolveWhenUsingFromTrustedIssuersThenReturnsAuthenticationManager() throws Exception {
      +	public void resolveWhenUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception {
       		try (MockWebServer server = new MockWebServer()) {
       			server.start();
       			String issuer = server.url("").toString();
      @@ -73,7 +72,7 @@ public class JwtIssuerAuthenticationManagerResolverTests {
       			server.enqueue(new MockResponse().setResponseCode(200)
       					.setHeader("Content-Type", "application/json")
       					.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)
      -					));
      +			));
       			server.enqueue(new MockResponse().setResponseCode(200)
       					.setHeader("Content-Type", "application/json")
       					.setBody(JWK_SET)
      @@ -86,40 +85,8 @@ public class JwtIssuerAuthenticationManagerResolverTests {
       			JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
       					new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer))));
       			jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
      -			JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver
      -				.fromTrustedIssuers(issuer);
      -			Authentication token = withBearerToken(jws.serialize());
      -			AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null);
      -			assertThat(authenticationManager).isNotNull();
      -			Authentication authentication = authenticationManager.authenticate(token);
      -			assertThat(authentication.isAuthenticated()).isTrue();
      -		}
      -	}
      -
      -	@Test
      -	public void resolveWhenUsingFromTrustedIssuersPredicateThenReturnsAuthenticationManager() throws Exception {
      -		try (MockWebServer server = new MockWebServer()) {
      -			server.start();
      -			String issuer = server.url("").toString();
      -			// @formatter:off
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -					.setHeader("Content-Type", "application/json")
      -					.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)
      -					));
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -					.setHeader("Content-Type", "application/json")
      -					.setBody(JWK_SET)
      -			);
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -					.setHeader("Content-Type", "application/json")
      -					.setBody(JWK_SET)
      -			);
      -			// @formatter:on
      -			JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
      -					new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer))));
      -			jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
      -			JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver
      -				.fromTrustedIssuers(issuer::equals);
      +			JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      +					issuer);
       			Authentication token = withBearerToken(jws.serialize());
       			AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null);
       			assertThat(authenticationManager).isNotNull();
      @@ -150,8 +117,8 @@ public class JwtIssuerAuthenticationManagerResolverTests {
       			JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
       					new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer))));
       			jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
      -			JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver
      -				.fromTrustedIssuers(issuer);
      +			JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      +					issuer);
       			Authentication token = withBearerToken(jws.serialize());
       			AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null);
       			assertThat(authenticationManager).isNotNull();
      @@ -182,8 +149,8 @@ public class JwtIssuerAuthenticationManagerResolverTests {
       
       	@Test
       	public void resolveWhenUsingUntrustedIssuerThenException() {
      -		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver
      -			.fromTrustedIssuers("other", "issuers");
      +		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      +				"other", "issuers");
       		Authentication token = withBearerToken(this.jwt);
       		// @formatter:off
       		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      @@ -227,8 +194,8 @@ public class JwtIssuerAuthenticationManagerResolverTests {
       
       	@Test
       	public void resolveWhenBearerTokenMalformedThenException() {
      -		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver
      -			.fromTrustedIssuers("trusted");
      +		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      +				"trusted");
       		Authentication token = withBearerToken("jwt");
       		// @formatter:off
       		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      @@ -239,8 +206,8 @@ public class JwtIssuerAuthenticationManagerResolverTests {
       
       	@Test
       	public void resolveWhenBearerTokenNoIssuerThenException() {
      -		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver
      -			.fromTrustedIssuers("trusted");
      +		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      +				"trusted");
       		Authentication token = withBearerToken(this.noIssuer);
       		// @formatter:off
       		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      @@ -251,8 +218,8 @@ public class JwtIssuerAuthenticationManagerResolverTests {
       
       	@Test
       	public void resolveWhenBearerTokenEvilThenGenericException() {
      -		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver
      -			.fromTrustedIssuers("trusted");
      +		JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
      +				"trusted");
       		Authentication token = withBearerToken(this.evil);
       		// @formatter:off
       		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      @@ -264,13 +231,11 @@ public class JwtIssuerAuthenticationManagerResolverTests {
       	}
       
       	@Test
      -	public void factoryWhenNullOrEmptyIssuersThenException() {
      +	public void constructorWhenNullOrEmptyIssuersThenException() {
       		assertThatIllegalArgumentException()
      -			.isThrownBy(() -> JwtIssuerAuthenticationManagerResolver.fromTrustedIssuers((Predicate) null));
      +			.isThrownBy(() -> new JwtIssuerAuthenticationManagerResolver((Collection) null));
       		assertThatIllegalArgumentException()
      -			.isThrownBy(() -> JwtIssuerAuthenticationManagerResolver.fromTrustedIssuers((Collection) null));
      -		assertThatIllegalArgumentException()
      -			.isThrownBy(() -> JwtIssuerAuthenticationManagerResolver.fromTrustedIssuers(Collections.emptyList()));
      +			.isThrownBy(() -> new JwtIssuerAuthenticationManagerResolver(Collections.emptyList()));
       	}
       
       	@Test
      diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverDeprecatedTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverDeprecatedTests.java
      deleted file mode 100644
      index 9e0c08f1a6..0000000000
      --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverDeprecatedTests.java
      +++ /dev/null
      @@ -1,261 +0,0 @@
      -/*
      - * Copyright 2002-2020 the original author or authors.
      - *
      - * Licensed under the Apache License, Version 2.0 (the "License");
      - * you may not use this file except in compliance with the License.
      - * You may obtain a copy of the License at
      - *
      - *      https://www.apache.org/licenses/LICENSE-2.0
      - *
      - * Unless required by applicable law or agreed to in writing, software
      - * distributed under the License is distributed on an "AS IS" BASIS,
      - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      - * See the License for the specific language governing permissions and
      - * limitations under the License.
      - */
      -
      -package org.springframework.security.oauth2.server.resource.authentication;
      -
      -import java.util.Collection;
      -import java.util.Collections;
      -import java.util.HashMap;
      -import java.util.Map;
      -
      -import com.nimbusds.jose.JWSAlgorithm;
      -import com.nimbusds.jose.JWSHeader;
      -import com.nimbusds.jose.JWSObject;
      -import com.nimbusds.jose.Payload;
      -import com.nimbusds.jose.crypto.RSASSASigner;
      -import com.nimbusds.jwt.JWTClaimsSet;
      -import com.nimbusds.jwt.PlainJWT;
      -import net.minidev.json.JSONObject;
      -import okhttp3.mockwebserver.MockResponse;
      -import okhttp3.mockwebserver.MockWebServer;
      -import org.junit.jupiter.api.Test;
      -import reactor.core.publisher.Mono;
      -
      -import org.springframework.security.authentication.ReactiveAuthenticationManager;
      -import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
      -import org.springframework.security.core.Authentication;
      -import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
      -import org.springframework.security.oauth2.jose.TestKeys;
      -import org.springframework.security.oauth2.jwt.JwtClaimNames;
      -import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerReactiveAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver;
      -
      -import static org.assertj.core.api.Assertions.assertThat;
      -import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
      -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
      -import static org.mockito.BDDMockito.any;
      -import static org.mockito.BDDMockito.given;
      -import static org.mockito.BDDMockito.mock;
      -import static org.mockito.BDDMockito.verify;
      -
      -/**
      - * Tests for {@link JwtIssuerReactiveAuthenticationManagerResolver}
      - */
      -@Deprecated
      -public class JwtIssuerReactiveAuthenticationManagerResolverDeprecatedTests {
      -
      -	// @formatter:off
      -	private static final String DEFAULT_RESPONSE_TEMPLATE = "{\n"
      -			+ "    \"issuer\": \"%s\", \n"
      -			+ "    \"jwks_uri\": \"%s/.well-known/jwks.json\" \n"
      -			+ "}";
      -	// @formatter:on
      -
      -	private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"3FlqJr5TRskIQIgdE3Dd7D9lboWdcTUT8a-fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRvc5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4_1tfRgG6ii4Uhxh6iI8qNMJQX-fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2kJdJ_ZIV-WW4noDdzpKqHcwmB8FsrumlVY_DNVvUSDIipiq9PbP4H99TXN1o746oRaNa07rq1hoCgMSSy-85SagCoxlmyE-D-of9SsMY8Ol9t0rdzpobBuhyJ_o5dfvjKw\"}]}";
      -
      -	private String jwt = jwt("iss", "trusted");
      -
      -	private String evil = jwt("iss", "\"");
      -
      -	private String noIssuer = jwt("sub", "sub");
      -
      -	@Test
      -	public void resolveWhenUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception {
      -		try (MockWebServer server = new MockWebServer()) {
      -			String issuer = server.url("").toString();
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -				.setHeader("Content-Type", "application/json")
      -				.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)));
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -				.setHeader("Content-Type", "application/json")
      -				.setBody(JWK_SET));
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -				.setHeader("Content-Type", "application/json")
      -				.setBody(JWK_SET));
      -			JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
      -					new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer))));
      -			jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
      -			JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      -					issuer);
      -			ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null).block();
      -			assertThat(authenticationManager).isNotNull();
      -			BearerTokenAuthenticationToken token = withBearerToken(jws.serialize());
      -			Authentication authentication = authenticationManager.authenticate(token).block();
      -			assertThat(authentication).isNotNull();
      -			assertThat(authentication.isAuthenticated()).isTrue();
      -		}
      -	}
      -
      -	// gh-10444
      -	@Test
      -	public void resolveWhednUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception {
      -		try (MockWebServer server = new MockWebServer()) {
      -			String issuer = server.url("").toString();
      -			// @formatter:off
      -			server.enqueue(new MockResponse().setResponseCode(500).setHeader("Content-Type", "application/json")
      -					.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)));
      -			server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json")
      -					.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)));
      -			server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json")
      -					.setBody(JWK_SET));
      -			// @formatter:on
      -			JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
      -					new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer))));
      -			jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
      -			JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      -					issuer);
      -			ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null).block();
      -			assertThat(authenticationManager).isNotNull();
      -			Authentication token = withBearerToken(jws.serialize());
      -			assertThatExceptionOfType(IllegalArgumentException.class)
      -				.isThrownBy(() -> authenticationManager.authenticate(token).block());
      -			Authentication authentication = authenticationManager.authenticate(token).block();
      -			assertThat(authentication.isAuthenticated()).isTrue();
      -		}
      -	}
      -
      -	@Test
      -	public void resolveWhenUsingSameIssuerThenReturnsSameAuthenticationManager() throws Exception {
      -		try (MockWebServer server = new MockWebServer()) {
      -			String issuer = server.url("").toString();
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -				.setHeader("Content-Type", "application/json")
      -				.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)));
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -				.setHeader("Content-Type", "application/json")
      -				.setBody(JWK_SET));
      -			TrustedIssuerJwtAuthenticationManagerResolver resolver = new TrustedIssuerJwtAuthenticationManagerResolver(
      -					(iss) -> iss.equals(issuer));
      -			ReactiveAuthenticationManager authenticationManager = resolver.resolve(issuer).block();
      -			ReactiveAuthenticationManager cachedAuthenticationManager = resolver.resolve(issuer).block();
      -			assertThat(authenticationManager).isSameAs(cachedAuthenticationManager);
      -		}
      -	}
      -
      -	@Test
      -	public void resolveWhenUsingUntrustedIssuerThenException() {
      -		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      -				"other", "issuers");
      -		Authentication token = withBearerToken(this.jwt);
      -		// @formatter:off
      -		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      -				.isThrownBy(() -> authenticationManagerResolver.resolve(null)
      -						.flatMap((authenticationManager) -> authenticationManager.authenticate(token))
      -						.block())
      -				.withMessageContaining("Invalid issuer");
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverThenUses() {
      -		Authentication token = withBearerToken(this.jwt);
      -		ReactiveAuthenticationManager authenticationManager = mock(ReactiveAuthenticationManager.class);
      -		given(authenticationManager.authenticate(token)).willReturn(Mono.empty());
      -		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      -				(issuer) -> Mono.just(authenticationManager));
      -		authenticationManagerResolver.resolve(null).flatMap((manager) -> manager.authenticate(token)).block();
      -		verify(authenticationManager).authenticate(any());
      -	}
      -
      -	@Test
      -	public void resolveWhenUsingExternalSourceThenRespondsToChanges() {
      -		Authentication token = withBearerToken(this.jwt);
      -		Map authenticationManagers = new HashMap<>();
      -		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      -				(issuer) -> Mono.justOrEmpty(authenticationManagers.get(issuer)));
      -		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      -			.isThrownBy(() -> authenticationManagerResolver.resolve(null)
      -				.flatMap((manager) -> manager.authenticate(token))
      -				.block())
      -			.withMessageContaining("Invalid issuer");
      -		ReactiveAuthenticationManager authenticationManager = mock(ReactiveAuthenticationManager.class);
      -		given(authenticationManager.authenticate(token)).willReturn(Mono.empty());
      -		authenticationManagers.put("trusted", authenticationManager);
      -		authenticationManagerResolver.resolve(null).flatMap((manager) -> manager.authenticate(token)).block();
      -		verify(authenticationManager).authenticate(token);
      -		authenticationManagers.clear();
      -		// @formatter:off
      -		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      -				.isThrownBy(() -> authenticationManagerResolver.resolve(null)
      -						.flatMap((manager) -> manager.authenticate(token))
      -						.block())
      -				.withMessageContaining("Invalid issuer");
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void resolveWhenBearerTokenMalformedThenException() {
      -		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      -				"trusted");
      -		Authentication token = withBearerToken("jwt");
      -		// @formatter:off
      -		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      -				.isThrownBy(() -> authenticationManagerResolver.resolve(null)
      -						.flatMap((manager) -> manager.authenticate(token))
      -						.block())
      -				.withMessageNotContaining("Invalid issuer");
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void resolveWhenBearerTokenNoIssuerThenException() {
      -		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      -				"trusted");
      -		Authentication token = withBearerToken(this.noIssuer);
      -		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      -			.isThrownBy(() -> authenticationManagerResolver.resolve(null)
      -				.flatMap((manager) -> manager.authenticate(token))
      -				.block())
      -			.withMessageContaining("Missing issuer");
      -	}
      -
      -	@Test
      -	public void resolveWhenBearerTokenEvilThenGenericException() {
      -		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      -				"trusted");
      -		Authentication token = withBearerToken(this.evil);
      -		// @formatter:off
      -		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      -				.isThrownBy(() -> authenticationManagerResolver.resolve(null)
      -						.flatMap((manager) -> manager.authenticate(token))
      -						.block())
      -				.withMessage("Invalid token");
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void constructorWhenNullOrEmptyIssuersThenException() {
      -		assertThatIllegalArgumentException()
      -			.isThrownBy(() -> new JwtIssuerReactiveAuthenticationManagerResolver((Collection) null));
      -		assertThatIllegalArgumentException()
      -			.isThrownBy(() -> new JwtIssuerReactiveAuthenticationManagerResolver(Collections.emptyList()));
      -	}
      -
      -	@Test
      -	public void constructorWhenNullAuthenticationManagerResolverThenException() {
      -		assertThatIllegalArgumentException().isThrownBy(
      -				() -> new JwtIssuerReactiveAuthenticationManagerResolver((ReactiveAuthenticationManagerResolver) null));
      -	}
      -
      -	private String jwt(String claim, String value) {
      -		PlainJWT jwt = new PlainJWT(new JWTClaimsSet.Builder().claim(claim, value).build());
      -		return jwt.serialize();
      -	}
      -
      -	private BearerTokenAuthenticationToken withBearerToken(String token) {
      -		return new BearerTokenAuthenticationToken(token);
      -	}
      -
      -}
      diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java
      index f12c6d65be..a830cbdfd8 100644
      --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java
      +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java
      @@ -20,7 +20,6 @@ import java.util.Collection;
       import java.util.Collections;
       import java.util.HashMap;
       import java.util.Map;
      -import java.util.function.Predicate;
       
       import com.nimbusds.jose.JWSAlgorithm;
       import com.nimbusds.jose.JWSHeader;
      @@ -72,7 +71,7 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests {
       	private String noIssuer = jwt("sub", "sub");
       
       	@Test
      -	public void resolveWhenUsingFromTrustedIssuersThenReturnsAuthenticationManager() throws Exception {
      +	public void resolveWhenUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception {
       		try (MockWebServer server = new MockWebServer()) {
       			String issuer = server.url("").toString();
       			server.enqueue(new MockResponse().setResponseCode(200)
      @@ -87,35 +86,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests {
       			JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
       					new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer))));
       			jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
      -			JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver
      -				.fromTrustedIssuers(issuer);
      -			ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null).block();
      -			assertThat(authenticationManager).isNotNull();
      -			BearerTokenAuthenticationToken token = withBearerToken(jws.serialize());
      -			Authentication authentication = authenticationManager.authenticate(token).block();
      -			assertThat(authentication).isNotNull();
      -			assertThat(authentication.isAuthenticated()).isTrue();
      -		}
      -	}
      -
      -	@Test
      -	public void resolveWhenUsingFromTrustedIssuersPredicateThenReturnsAuthenticationManager() throws Exception {
      -		try (MockWebServer server = new MockWebServer()) {
      -			String issuer = server.url("").toString();
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -				.setHeader("Content-Type", "application/json")
      -				.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)));
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -				.setHeader("Content-Type", "application/json")
      -				.setBody(JWK_SET));
      -			server.enqueue(new MockResponse().setResponseCode(200)
      -				.setHeader("Content-Type", "application/json")
      -				.setBody(JWK_SET));
      -			JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
      -					new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer))));
      -			jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
      -			JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver
      -				.fromTrustedIssuers(issuer::equals);
      +			JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      +					issuer);
       			ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null).block();
       			assertThat(authenticationManager).isNotNull();
       			BearerTokenAuthenticationToken token = withBearerToken(jws.serialize());
      @@ -141,8 +113,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests {
       			JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
       					new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer))));
       			jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
      -			JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver
      -				.fromTrustedIssuers(issuer);
      +			JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      +					issuer);
       			ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null).block();
       			assertThat(authenticationManager).isNotNull();
       			Authentication token = withBearerToken(jws.serialize());
      @@ -173,8 +145,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests {
       
       	@Test
       	public void resolveWhenUsingUntrustedIssuerThenException() {
      -		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver
      -			.fromTrustedIssuers("other", "issuers");
      +		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      +				"other", "issuers");
       		Authentication token = withBearerToken(this.jwt);
       		// @formatter:off
       		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      @@ -224,8 +196,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests {
       
       	@Test
       	public void resolveWhenBearerTokenMalformedThenException() {
      -		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver
      -			.fromTrustedIssuers("trusted");
      +		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      +				"trusted");
       		Authentication token = withBearerToken("jwt");
       		// @formatter:off
       		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      @@ -238,8 +210,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests {
       
       	@Test
       	public void resolveWhenBearerTokenNoIssuerThenException() {
      -		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver
      -			.fromTrustedIssuers("trusted");
      +		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      +				"trusted");
       		Authentication token = withBearerToken(this.noIssuer);
       		assertThatExceptionOfType(OAuth2AuthenticationException.class)
       			.isThrownBy(() -> authenticationManagerResolver.resolve(null)
      @@ -250,8 +222,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests {
       
       	@Test
       	public void resolveWhenBearerTokenEvilThenGenericException() {
      -		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver
      -			.fromTrustedIssuers("trusted");
      +		JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver(
      +				"trusted");
       		Authentication token = withBearerToken(this.evil);
       		// @formatter:off
       		assertThatExceptionOfType(OAuth2AuthenticationException.class)
      @@ -263,13 +235,11 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests {
       	}
       
       	@Test
      -	public void factoryWhenNullOrEmptyIssuersThenException() {
      -		assertThatIllegalArgumentException().isThrownBy(
      -				() -> JwtIssuerReactiveAuthenticationManagerResolver.fromTrustedIssuers((Predicate) null));
      -		assertThatIllegalArgumentException().isThrownBy(
      -				() -> JwtIssuerReactiveAuthenticationManagerResolver.fromTrustedIssuers((Collection) null));
      -		assertThatIllegalArgumentException().isThrownBy(
      -				() -> JwtIssuerReactiveAuthenticationManagerResolver.fromTrustedIssuers(Collections.emptyList()));
      +	public void constructorWhenNullOrEmptyIssuersThenException() {
      +		assertThatIllegalArgumentException()
      +			.isThrownBy(() -> new JwtIssuerReactiveAuthenticationManagerResolver((Collection) null));
      +		assertThatIllegalArgumentException()
      +			.isThrownBy(() -> new JwtIssuerReactiveAuthenticationManagerResolver(Collections.emptyList()));
       	}
       
       	@Test
      diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterTests.java
      index 31f250ab16..7e5b7ef59e 100644
      --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterTests.java
      +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverterTests.java
      @@ -1,5 +1,5 @@
       /*
      - * Copyright 2002-2023 the original author or authors.
      + * 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.
      @@ -35,7 +35,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
        * Tests for {@link ReactiveJwtAuthenticationConverter}
        *
        * @author Eric Deandrea
      - * @author Marcus Kainth
        * @since 5.2
        */
       public class ReactiveJwtAuthenticationConverterTests {
      @@ -69,47 +68,4 @@ public class ReactiveJwtAuthenticationConverterTests {
       		assertThat(authorities).containsExactly(new SimpleGrantedAuthority("blah"));
       	}
       
      -	@Test
      -	public void whenSettingNullPrincipalClaimName() {
      -		// @formatter:off
      -		assertThatIllegalArgumentException()
      -				.isThrownBy(() -> this.jwtAuthenticationConverter.setPrincipalClaimName(null))
      -				.withMessage("principalClaimName cannot be empty");
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void whenSettingEmptyPrincipalClaimName() {
      -		// @formatter:off
      -		assertThatIllegalArgumentException()
      -				.isThrownBy(() -> this.jwtAuthenticationConverter.setPrincipalClaimName(""))
      -				.withMessage("principalClaimName cannot be empty");
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void whenSettingBlankPrincipalClaimName() {
      -		// @formatter:off
      -		assertThatIllegalArgumentException()
      -				.isThrownBy(() -> this.jwtAuthenticationConverter.setPrincipalClaimName(" "))
      -				.withMessage("principalClaimName cannot be empty");
      -		// @formatter:on
      -	}
      -
      -	@Test
      -	public void convertWhenPrincipalClaimNameSet() {
      -		this.jwtAuthenticationConverter.setPrincipalClaimName("user_id");
      -		Jwt jwt = TestJwts.jwt().claim("user_id", "100").build();
      -		AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt).block();
      -		assertThat(authentication.getName()).isEqualTo("100");
      -	}
      -
      -	@Test
      -	public void convertWhenPrincipalClaimNameSetAndClaimValueIsNotString() {
      -		this.jwtAuthenticationConverter.setPrincipalClaimName("user_id");
      -		Jwt jwt = TestJwts.jwt().claim("user_id", 100).build();
      -		AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt).block();
      -		assertThat(authentication.getName()).isEqualTo("100");
      -	}
      -
       }
      diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/NimbusReactiveOpaqueTokenIntrospectorTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/NimbusReactiveOpaqueTokenIntrospectorTests.java
      index 68ff78a2ac..d4ae7111c6 100644
      --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/NimbusReactiveOpaqueTokenIntrospectorTests.java
      +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/NimbusReactiveOpaqueTokenIntrospectorTests.java
      @@ -270,6 +270,7 @@ public class NimbusReactiveOpaqueTokenIntrospectorTests {
       		WebClient webClient = spy(WebClient.class);
       		given(webClient.post()).willReturn(spec);
       		ClientResponse clientResponse = mock(ClientResponse.class);
      +		given(clientResponse.rawStatusCode()).willReturn(200);
       		given(clientResponse.statusCode()).willReturn(HttpStatus.OK);
       		given(clientResponse.bodyToMono(String.class)).willReturn(Mono.just(response));
       		ClientResponse.Headers headers = mock(ClientResponse.Headers.class);
      diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java
      index f395fd6326..974565affd 100644
      --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java
      +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java
      @@ -227,6 +227,7 @@ public class SpringReactiveOpaqueTokenIntrospectorTests {
       		WebClient webClient = spy(WebClient.class);
       		given(webClient.post()).willReturn(spec);
       		ClientResponse clientResponse = mock(ClientResponse.class);
      +		given(clientResponse.rawStatusCode()).willReturn(200);
       		given(clientResponse.statusCode()).willReturn(HttpStatus.OK);
       		given(clientResponse.bodyToMono(STRING_OBJECT_MAP)).willReturn(Mono.just(response));
       		ClientResponse.Headers headers = mock(ClientResponse.Headers.class);
      diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java
      index e5cfca01c3..f1a672aca3 100644
      --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java
      +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java
      @@ -121,7 +121,6 @@ public class DefaultBearerTokenResolverTests {
       		MockHttpServletRequest request = new MockHttpServletRequest();
       		request.addHeader("Authorization", "Bearer " + TEST_TOKEN);
       		request.setMethod("GET");
      -		request.setQueryString("access_token=" + TEST_TOKEN);
       		request.addParameter("access_token", TEST_TOKEN);
       		assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.resolver.resolve(request))
       			.withMessageContaining("Found multiple bearer tokens in the request");
      @@ -160,7 +159,7 @@ public class DefaultBearerTokenResolverTests {
       	}
       
       	@Test
      -	public void resolveWhenPostAndFormParameterIsPresentAndSupportedThenTokenIsResolved() {
      +	public void resolveWhenFormParameterIsPresentAndSupportedThenTokenIsResolved() {
       		this.resolver.setAllowFormEncodedBodyParameter(true);
       		MockHttpServletRequest request = new MockHttpServletRequest();
       		request.setMethod("POST");
      @@ -169,67 +168,6 @@ public class DefaultBearerTokenResolverTests {
       		assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
       	}
       
      -	@Test
      -	public void resolveWhenPutAndFormParameterIsPresentAndSupportedThenTokenIsResolved() {
      -		this.resolver.setAllowFormEncodedBodyParameter(true);
      -
      -		MockHttpServletRequest request = new MockHttpServletRequest();
      -		request.setMethod("PUT");
      -		request.setContentType("application/x-www-form-urlencoded");
      -		request.addParameter("access_token", TEST_TOKEN);
      -
      -		assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
      -	}
      -
      -	@Test
      -	public void resolveWhenPatchAndFormParameterIsPresentAndSupportedThenTokenIsResolved() {
      -		this.resolver.setAllowFormEncodedBodyParameter(true);
      -
      -		MockHttpServletRequest request = new MockHttpServletRequest();
      -		request.setMethod("PATCH");
      -		request.setContentType("application/x-www-form-urlencoded");
      -		request.addParameter("access_token", TEST_TOKEN);
      -
      -		assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
      -	}
      -
      -	@Test
      -	public void resolveWhenDeleteAndFormParameterIsPresentAndSupportedThenTokenIsResolved() {
      -		this.resolver.setAllowFormEncodedBodyParameter(true);
      -
      -		MockHttpServletRequest request = new MockHttpServletRequest();
      -		request.setMethod("DELETE");
      -		request.setContentType("application/x-www-form-urlencoded");
      -		request.addParameter("access_token", TEST_TOKEN);
      -
      -		assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
      -	}
      -
      -	@Test
      -	public void resolveWhenGetAndFormParameterIsPresentAndSupportedThenTokenIsNotResolved() {
      -		this.resolver.setAllowFormEncodedBodyParameter(true);
      -
      -		MockHttpServletRequest request = new MockHttpServletRequest();
      -		request.setMethod("GET");
      -		request.setContentType("application/x-www-form-urlencoded");
      -		request.addParameter("access_token", TEST_TOKEN);
      -
      -		assertThat(this.resolver.resolve(request)).isNull();
      -	}
      -
      -	@Test
      -	public void resolveWhenPostAndFormParameterIsSupportedAndQueryParameterIsPresentThenTokenIsNotResolved() {
      -		this.resolver.setAllowFormEncodedBodyParameter(true);
      -
      -		MockHttpServletRequest request = new MockHttpServletRequest();
      -		request.setMethod("POST");
      -		request.setContentType("application/x-www-form-urlencoded");
      -		request.setQueryString("access_token=" + TEST_TOKEN);
      -		request.addParameter("access_token", TEST_TOKEN);
      -
      -		assertThat(this.resolver.resolve(request)).isNull();
      -	}
      -
       	@Test
       	public void resolveWhenFormParameterIsPresentAndNotSupportedThenTokenIsNotResolved() {
       		MockHttpServletRequest request = new MockHttpServletRequest();
      @@ -244,7 +182,6 @@ public class DefaultBearerTokenResolverTests {
       		this.resolver.setAllowUriQueryParameter(true);
       		MockHttpServletRequest request = new MockHttpServletRequest();
       		request.setMethod("GET");
      -		request.setQueryString("access_token=" + TEST_TOKEN);
       		request.addParameter("access_token", TEST_TOKEN);
       		assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN);
       	}
      @@ -253,7 +190,6 @@ public class DefaultBearerTokenResolverTests {
       	public void resolveWhenQueryParameterIsPresentAndNotSupportedThenTokenIsNotResolved() {
       		MockHttpServletRequest request = new MockHttpServletRequest();
       		request.setMethod("GET");
      -		request.setQueryString("access_token=" + TEST_TOKEN);
       		request.addParameter("access_token", TEST_TOKEN);
       		assertThat(this.resolver.resolve(request)).isNull();
       	}
      diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/core/PayloadInterceptorRSocket.java b/rsocket/src/main/java/org/springframework/security/rsocket/core/PayloadInterceptorRSocket.java
      index cb9efc01fb..6bd1c674f7 100644
      --- a/rsocket/src/main/java/org/springframework/security/rsocket/core/PayloadInterceptorRSocket.java
      +++ b/rsocket/src/main/java/org/springframework/security/rsocket/core/PayloadInterceptorRSocket.java
      @@ -94,7 +94,7 @@ class PayloadInterceptorRSocket extends RSocketProxy {
       			return intercept(PayloadExchangeType.REQUEST_CHANNEL, firstPayload)
       				.flatMapMany((context) -> innerFlux.index()
       					.concatMap((tuple) -> justOrIntercept(tuple.getT1(), tuple.getT2()))
      -					.transform(this.source::requestChannel)
      +					.transform((securedPayloads) -> this.source.requestChannel(securedPayloads))
       					.contextWrite(context));
       		});
       	}
      @@ -115,7 +115,7 @@ class PayloadInterceptorRSocket extends RSocketProxy {
       			DefaultPayloadExchange exchange = new DefaultPayloadExchange(type, payload, this.metadataMimeType,
       					this.dataMimeType);
       			return chain.next(exchange)
      -				.then(Mono.fromCallable(chain::getContext))
      +				.then(Mono.fromCallable(() -> chain.getContext()))
       				.defaultIfEmpty(Context.empty())
       				.contextWrite(this.context);
       		});
      diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/core/PayloadSocketAcceptor.java b/rsocket/src/main/java/org/springframework/security/rsocket/core/PayloadSocketAcceptor.java
      index 2270964f1c..ccc9b5776b 100644
      --- a/rsocket/src/main/java/org/springframework/security/rsocket/core/PayloadSocketAcceptor.java
      +++ b/rsocket/src/main/java/org/springframework/security/rsocket/core/PayloadSocketAcceptor.java
      @@ -81,7 +81,9 @@ class PayloadSocketAcceptor implements SocketAcceptor {
       			ContextPayloadInterceptorChain chain = new ContextPayloadInterceptorChain(this.interceptors);
       			DefaultPayloadExchange exchange = new DefaultPayloadExchange(PayloadExchangeType.SETUP, payload,
       					metadataMimeType, dataMimeType);
      -			return chain.next(exchange).then(Mono.fromCallable(chain::getContext)).defaultIfEmpty(Context.empty());
      +			return chain.next(exchange)
      +				.then(Mono.fromCallable(() -> chain.getContext()))
      +				.defaultIfEmpty(Context.empty());
       		});
       	}
       
      diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/util/matcher/PayloadExchangeMatchers.java b/rsocket/src/main/java/org/springframework/security/rsocket/util/matcher/PayloadExchangeMatchers.java
      index d47cd44d86..ef96a1b1c9 100644
      --- a/rsocket/src/main/java/org/springframework/security/rsocket/util/matcher/PayloadExchangeMatchers.java
      +++ b/rsocket/src/main/java/org/springframework/security/rsocket/util/matcher/PayloadExchangeMatchers.java
      @@ -16,6 +16,9 @@
       
       package org.springframework.security.rsocket.util.matcher;
       
      +import reactor.core.publisher.Mono;
      +
      +import org.springframework.security.rsocket.api.PayloadExchange;
       import org.springframework.security.rsocket.api.PayloadExchangeType;
       
       /**
      @@ -27,17 +30,37 @@ public final class PayloadExchangeMatchers {
       	}
       
       	public static PayloadExchangeMatcher setup() {
      -		return (exchange) -> PayloadExchangeType.SETUP.equals(exchange.getType())
      -				? PayloadExchangeMatcher.MatchResult.match() : PayloadExchangeMatcher.MatchResult.notMatch();
      +		return new PayloadExchangeMatcher() {
      +
      +			@Override
      +			public Mono matches(PayloadExchange exchange) {
      +				return PayloadExchangeType.SETUP.equals(exchange.getType()) ? MatchResult.match()
      +						: MatchResult.notMatch();
      +			}
      +
      +		};
       	}
       
       	public static PayloadExchangeMatcher anyRequest() {
      -		return (exchange) -> exchange.getType().isRequest() ? PayloadExchangeMatcher.MatchResult.match()
      -				: PayloadExchangeMatcher.MatchResult.notMatch();
      +		return new PayloadExchangeMatcher() {
      +
      +			@Override
      +			public Mono matches(PayloadExchange exchange) {
      +				return exchange.getType().isRequest() ? MatchResult.match() : MatchResult.notMatch();
      +			}
      +
      +		};
       	}
       
       	public static PayloadExchangeMatcher anyExchange() {
      -		return (exchange) -> PayloadExchangeMatcher.MatchResult.match();
      +		return new PayloadExchangeMatcher() {
      +
      +			@Override
      +			public Mono matches(PayloadExchange exchange) {
      +				return MatchResult.match();
      +			}
      +
      +		};
       	}
       
       }
      diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/util/matcher/RoutePayloadExchangeMatcher.java b/rsocket/src/main/java/org/springframework/security/rsocket/util/matcher/RoutePayloadExchangeMatcher.java
      index 45f4e7b303..aad1522ddd 100644
      --- a/rsocket/src/main/java/org/springframework/security/rsocket/util/matcher/RoutePayloadExchangeMatcher.java
      +++ b/rsocket/src/main/java/org/springframework/security/rsocket/util/matcher/RoutePayloadExchangeMatcher.java
      @@ -52,9 +52,9 @@ public class RoutePayloadExchangeMatcher implements PayloadExchangeMatcher {
       		Map metadata = this.metadataExtractor.extract(exchange.getPayload(),
       				exchange.getMetadataMimeType());
       		return Optional.ofNullable((String) metadata.get(MetadataExtractor.ROUTE_KEY))
      -			.map(this.routeMatcher::parseRoute)
      +			.map((routeValue) -> this.routeMatcher.parseRoute(routeValue))
       			.map((route) -> this.routeMatcher.matchAndExtract(this.pattern, route))
      -			.map(MatchResult::match)
      +			.map((v) -> MatchResult.match(v))
       			.orElse(MatchResult.notMatch());
       	}
       
      diff --git a/rsocket/src/test/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadInterceptorTests.java b/rsocket/src/test/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadInterceptorTests.java
      index 070c400a84..e41455f2f7 100644
      --- a/rsocket/src/test/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadInterceptorTests.java
      +++ b/rsocket/src/test/java/org/springframework/security/rsocket/authentication/AuthenticationPayloadInterceptorTests.java
      @@ -89,8 +89,8 @@ public class AuthenticationPayloadInterceptorTests {
       		interceptor.intercept(exchange, authenticationPayloadChain).block();
       		Authentication authentication = authenticationPayloadChain.getAuthentication();
       		verify(this.authenticationManager).authenticate(this.authenticationArg.capture());
      -		assertThat(this.authenticationArg.getValue()).usingRecursiveComparison()
      -			.isEqualTo(UsernamePasswordAuthenticationToken.unauthenticated("user", "password"));
      +		assertThat(this.authenticationArg.getValue())
      +			.isEqualToComparingFieldByField(UsernamePasswordAuthenticationToken.unauthenticated("user", "password"));
       		assertThat(authentication).isEqualTo(expectedAuthentication);
       	}
       
      diff --git a/rsocket/src/test/java/org/springframework/security/rsocket/metadata/BasicAuthenticationDecoderTests.java b/rsocket/src/test/java/org/springframework/security/rsocket/metadata/BasicAuthenticationDecoderTests.java
      index 1ae60bb652..7c38154f79 100644
      --- a/rsocket/src/test/java/org/springframework/security/rsocket/metadata/BasicAuthenticationDecoderTests.java
      +++ b/rsocket/src/test/java/org/springframework/security/rsocket/metadata/BasicAuthenticationDecoderTests.java
      @@ -46,7 +46,7 @@ public class BasicAuthenticationDecoderTests {
       		UsernamePasswordMetadata actualCredentials = decoder
       			.decodeToMono(Mono.just(dataBuffer), elementType, mimeType, hints)
       			.block();
      -		assertThat(actualCredentials).usingRecursiveComparison().isEqualTo(expectedCredentials);
      +		assertThat(actualCredentials).isEqualToComparingFieldByField(expectedCredentials);
       	}
       
       }
      diff --git a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle
      index bc6fedd828..e00593c149 100644
      --- a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle
      +++ b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle
      @@ -22,6 +22,7 @@ dependencies {
       	testImplementation "org.junit.jupiter:junit-jupiter-params"
       	testImplementation "org.junit.jupiter:junit-jupiter-engine"
       	testImplementation "org.mockito:mockito-core"
      +	testImplementation "org.mockito:mockito-inline"
       	testImplementation "org.mockito:mockito-junit-jupiter"
       	testImplementation "org.springframework:spring-test"
       }
      diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java
      index 41cbabac4b..39f6b725d5 100644
      --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java
      +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java
      @@ -153,7 +153,7 @@ public abstract class AbstractSaml2AuthenticationRequest implements Serializable
       		/**
       		 * Creates a new Builder with relying party registration
       		 * @param registration the registration of the relying party.
      -		 * @since 5.8
      +		 * @sine 5.8
       		 */
       		protected Builder(RelyingPartyRegistration registration) {
       			this.relyingPartyRegistrationId = registration.getRegistrationId();
      diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java
      index 83ad3cace4..71ccdbadc4 100644
      --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java
      +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java
      @@ -1,5 +1,5 @@
       /*
      - * Copyright 2002-2023 the original author or authors.
      + * Copyright 2002-2022 the original author or authors.
        *
        * Licensed under the Apache License, Version 2.0 (the "License");
        * you may not use this file except in compliance with the License.
      @@ -37,6 +37,7 @@ import org.apache.commons.logging.LogFactory;
       import org.opensaml.core.config.ConfigurationService;
       import org.opensaml.core.xml.XMLObject;
       import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
      +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
       import org.opensaml.core.xml.schema.XSAny;
       import org.opensaml.core.xml.schema.XSBoolean;
       import org.opensaml.core.xml.schema.XSBooleanValue;
      @@ -88,6 +89,7 @@ import org.springframework.security.saml2.core.Saml2Error;
       import org.springframework.security.saml2.core.Saml2ErrorCodes;
       import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
       import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
      +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
       import org.springframework.util.Assert;
       import org.springframework.util.CollectionUtils;
       import org.springframework.util.LinkedMultiValueMap;
      @@ -409,15 +411,16 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
       		if (!StringUtils.hasText(inResponseTo)) {
       			return Saml2ResponseValidatorResult.success();
       		}
      -		if (storedRequest == null) {
      +		AuthnRequest request = parseRequest(storedRequest);
      +		if (request == null) {
       			String message = "The response contained an InResponseTo attribute [" + inResponseTo + "]"
       					+ " but no saved authentication request was found";
       			return Saml2ResponseValidatorResult
       				.failure(new Saml2Error(Saml2ErrorCodes.INVALID_IN_RESPONSE_TO, message));
       		}
      -		if (!inResponseTo.equals(storedRequest.getId())) {
      +		if (!inResponseTo.equals(request.getID())) {
       			String message = "The InResponseTo attribute [" + inResponseTo + "] does not match the ID of the "
      -					+ "authentication request [" + storedRequest.getId() + "]";
      +					+ "authentication request [" + request.getID() + "]";
       			return Saml2ResponseValidatorResult
       				.failure(new Saml2Error(Saml2ErrorCodes.INVALID_IN_RESPONSE_TO, message));
       		}
      @@ -774,7 +777,37 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
       	}
       
       	private static String getAuthnRequestId(AbstractSaml2AuthenticationRequest serialized) {
      -		return (serialized != null) ? serialized.getId() : null;
      +		AuthnRequest request = parseRequest(serialized);
      +		if (request == null) {
      +			return null;
      +		}
      +		return request.getID();
      +	}
      +
      +	private static AuthnRequest parseRequest(AbstractSaml2AuthenticationRequest request) {
      +		if (request == null) {
      +			return null;
      +		}
      +		String samlRequest = request.getSamlRequest();
      +		if (!StringUtils.hasText(samlRequest)) {
      +			return null;
      +		}
      +		if (request.getBinding() == Saml2MessageBinding.REDIRECT) {
      +			samlRequest = Saml2Utils.samlInflate(Saml2Utils.samlDecode(samlRequest));
      +		}
      +		else {
      +			samlRequest = new String(Saml2Utils.samlDecode(samlRequest), StandardCharsets.UTF_8);
      +		}
      +		try {
      +			Document document = XMLObjectProviderRegistrySupport.getParserPool()
      +				.parse(new ByteArrayInputStream(samlRequest.getBytes(StandardCharsets.UTF_8)));
      +			Element element = document.getDocumentElement();
      +			return (AuthnRequest) authnRequestUnmarshaller.unmarshall(element);
      +		}
      +		catch (Exception ex) {
      +			String message = "Failed to deserialize associated authentication request [" + ex.getMessage() + "]";
      +			throw createAuthenticationException(Saml2ErrorCodes.MALFORMED_REQUEST_DATA, message, ex);
      +		}
       	}
       
       	private static class SAML20AssertionValidators {
      diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java
      index e4b23b5000..e2c48ab984 100644
      --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java
      +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java
      @@ -1,5 +1,5 @@
       /*
      - * Copyright 2002-2023 the original author or authors.
      + * Copyright 2002-2022 the original author or authors.
        *
        * Licensed under the Apache License, Version 2.0 (the "License");
        * you may not use this file except in compliance with the License.
      @@ -30,13 +30,11 @@ import org.opensaml.core.xml.XMLObjectBuilder;
       import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
       import org.opensaml.saml.common.xml.SAMLConstants;
       import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
      -import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
       import org.opensaml.saml.saml2.metadata.EntityDescriptor;
       import org.opensaml.saml.saml2.metadata.KeyDescriptor;
       import org.opensaml.saml.saml2.metadata.NameIDFormat;
       import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
       import org.opensaml.saml.saml2.metadata.SingleLogoutService;
      -import org.opensaml.saml.saml2.metadata.impl.EntitiesDescriptorMarshaller;
       import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller;
       import org.opensaml.security.credential.UsageType;
       import org.opensaml.xmlsec.signature.KeyInfo;
      @@ -67,51 +65,25 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
       
       	private final EntityDescriptorMarshaller entityDescriptorMarshaller;
       
      -	private final EntitiesDescriptorMarshaller entitiesDescriptorMarshaller;
      -
       	private Consumer entityDescriptorCustomizer = (parameters) -> {
       	};
       
      -	private boolean usePrettyPrint = true;
      -
       	public OpenSamlMetadataResolver() {
       		this.entityDescriptorMarshaller = (EntityDescriptorMarshaller) XMLObjectProviderRegistrySupport
       			.getMarshallerFactory()
       			.getMarshaller(EntityDescriptor.DEFAULT_ELEMENT_NAME);
       		Assert.notNull(this.entityDescriptorMarshaller, "entityDescriptorMarshaller cannot be null");
      -		this.entitiesDescriptorMarshaller = (EntitiesDescriptorMarshaller) XMLObjectProviderRegistrySupport
      -			.getMarshallerFactory()
      -			.getMarshaller(EntitiesDescriptor.DEFAULT_ELEMENT_NAME);
      -		Assert.notNull(this.entitiesDescriptorMarshaller, "entitiesDescriptorMarshaller cannot be null");
       	}
       
       	@Override
       	public String resolve(RelyingPartyRegistration relyingPartyRegistration) {
      -		EntityDescriptor entityDescriptor = entityDescriptor(relyingPartyRegistration);
      -		return serialize(entityDescriptor);
      -	}
      -
      -	public String resolve(Iterable relyingPartyRegistrations) {
      -		Collection entityDescriptors = new ArrayList<>();
      -		for (RelyingPartyRegistration registration : relyingPartyRegistrations) {
      -			EntityDescriptor entityDescriptor = entityDescriptor(registration);
      -			entityDescriptors.add(entityDescriptor);
      -		}
      -		if (entityDescriptors.size() == 1) {
      -			return serialize(entityDescriptors.iterator().next());
      -		}
      -		EntitiesDescriptor entities = build(EntitiesDescriptor.DEFAULT_ELEMENT_NAME);
      -		entities.getEntityDescriptors().addAll(entityDescriptors);
      -		return serialize(entities);
      -	}
      -
      -	private EntityDescriptor entityDescriptor(RelyingPartyRegistration registration) {
       		EntityDescriptor entityDescriptor = build(EntityDescriptor.DEFAULT_ELEMENT_NAME);
      -		entityDescriptor.setEntityID(registration.getEntityId());
      -		SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(registration);
      +		entityDescriptor.setEntityID(relyingPartyRegistration.getEntityId());
      +		SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(relyingPartyRegistration);
       		entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor);
      -		this.entityDescriptorCustomizer.accept(new EntityDescriptorParameters(entityDescriptor, registration));
      -		return entityDescriptor;
      +		this.entityDescriptorCustomizer
      +			.accept(new EntityDescriptorParameters(entityDescriptor, relyingPartyRegistration));
      +		return serialize(entityDescriptor);
       	}
       
       	/**
      @@ -125,15 +97,6 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
       		this.entityDescriptorCustomizer = entityDescriptorCustomizer;
       	}
       
      -	/**
      -	 * Configure whether to pretty-print the metadata XML. This can be helpful when
      -	 * signing the metadata payload.
      -	 * @since 6.2
      -	 **/
      -	public void setUsePrettyPrint(boolean usePrettyPrint) {
      -		this.usePrettyPrint = usePrettyPrint;
      -	}
      -
       	private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registration) {
       		SPSSODescriptor spSsoDescriptor = build(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
       		spSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
      @@ -199,7 +162,7 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
       
       	private NameIDFormat buildNameIDFormat(RelyingPartyRegistration registration) {
       		NameIDFormat nameIdFormat = build(NameIDFormat.DEFAULT_ELEMENT_NAME);
      -		nameIdFormat.setURI(registration.getNameIdFormat());
      +		nameIdFormat.setFormat(registration.getNameIdFormat());
       		return nameIdFormat;
       	}
       
      @@ -215,23 +178,7 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
       	private String serialize(EntityDescriptor entityDescriptor) {
       		try {
       			Element element = this.entityDescriptorMarshaller.marshall(entityDescriptor);
      -			if (this.usePrettyPrint) {
      -				return SerializeSupport.prettyPrintXML(element);
      -			}
      -			return SerializeSupport.nodeToString(element);
      -		}
      -		catch (Exception ex) {
      -			throw new Saml2Exception(ex);
      -		}
      -	}
      -
      -	private String serialize(EntitiesDescriptor entities) {
      -		try {
      -			Element element = this.entitiesDescriptorMarshaller.marshall(entities);
      -			if (this.usePrettyPrint) {
      -				return SerializeSupport.prettyPrintXML(element);
      -			}
      -			return SerializeSupport.nodeToString(element);
      +			return SerializeSupport.prettyPrintXML(element);
       		}
       		catch (Exception ex) {
       			throw new Saml2Exception(ex);
      diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/RequestMatcherMetadataResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/RequestMatcherMetadataResponseResolver.java
      deleted file mode 100644
      index 42153be417..0000000000
      --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/RequestMatcherMetadataResponseResolver.java
      +++ /dev/null
      @@ -1,181 +0,0 @@
      -/*
      - * Copyright 2002-2023 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.saml2.provider.service.metadata;
      -
      -import java.io.UnsupportedEncodingException;
      -import java.net.URLEncoder;
      -import java.nio.charset.StandardCharsets;
      -import java.util.Collections;
      -import java.util.LinkedHashMap;
      -import java.util.Map;
      -import java.util.UUID;
      -
      -import jakarta.servlet.http.HttpServletRequest;
      -
      -import org.springframework.security.saml2.Saml2Exception;
      -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
      -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
      -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers;
      -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver;
      -import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
      -import org.springframework.security.web.util.matcher.OrRequestMatcher;
      -import org.springframework.security.web.util.matcher.RequestMatcher;
      -import org.springframework.util.Assert;
      -
      -/**
      - * An implementation of {@link Saml2MetadataResponseResolver} that identifies which
      - * {@link RelyingPartyRegistration}s to use with a {@link RequestMatcher}
      - *
      - * @author Josh Cummings
      - * @since 6.1
      - */
      -public final class RequestMatcherMetadataResponseResolver implements Saml2MetadataResponseResolver {
      -
      -	private static final String DEFAULT_METADATA_FILENAME = "saml-{registrationId}-metadata.xml";
      -
      -	private RequestMatcher matcher = new OrRequestMatcher(
      -			new AntPathRequestMatcher("/saml2/service-provider-metadata/{registrationId}"),
      -			new AntPathRequestMatcher("/saml2/metadata/{registrationId}"),
      -			new AntPathRequestMatcher("/saml2/metadata"));
      -
      -	private String filename = DEFAULT_METADATA_FILENAME;
      -
      -	private final RelyingPartyRegistrationRepository registrations;
      -
      -	private final Saml2MetadataResolver metadata;
      -
      -	/**
      -	 * Construct a {@link RequestMatcherMetadataResponseResolver}
      -	 * @param registrations the source for relying party metadata
      -	 * @param metadata the strategy for converting {@link RelyingPartyRegistration}s into
      -	 * metadata
      -	 */
      -	public RequestMatcherMetadataResponseResolver(RelyingPartyRegistrationRepository registrations,
      -			Saml2MetadataResolver metadata) {
      -		Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null");
      -		Assert.notNull(metadata, "saml2MetadataResolver cannot be null");
      -		this.registrations = registrations;
      -		this.metadata = metadata;
      -	}
      -
      -	/**
      -	 * Construct and serialize a relying party's SAML 2.0 metadata based on the given
      -	 * {@link HttpServletRequest}. Uses the configured {@link RequestMatcher} to identify
      -	 * the metadata request, including looking for any indicated {@code registrationId}.
      -	 *
      -	 * 

      - * If a {@code registrationId} is found in the request, it will attempt to use that, - * erroring if no {@link RelyingPartyRegistration} is found. - * - *

      - * If no {@code registrationId} is found in the request, it will attempt to show all - * {@link RelyingPartyRegistration}s in an {@code }. To - * exercise this functionality, the provided - * {@link RelyingPartyRegistrationRepository} needs to implement {@link Iterable}. - * @param request the HTTP request - * @return a {@link Saml2MetadataResponse} instance - * @throws Saml2Exception if the {@link RequestMatcher} specifies a non-existent - * {@code registrationId} - */ - @Override - public Saml2MetadataResponse resolve(HttpServletRequest request) { - RequestMatcher.MatchResult result = this.matcher.matcher(request); - if (!result.isMatch()) { - return null; - } - String registrationId = result.getVariables().get("registrationId"); - Saml2MetadataResponse response = responseByRegistrationId(request, registrationId); - if (response != null) { - return response; - } - if (this.registrations instanceof Iterable) { - Iterable registrations = (Iterable) this.registrations; - return responseByIterable(request, registrations); - } - return null; - } - - private Saml2MetadataResponse responseByRegistrationId(HttpServletRequest request, String registrationId) { - if (registrationId == null) { - return null; - } - RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId); - if (registration == null) { - throw new Saml2Exception("registration not found"); - } - return responseByIterable(request, Collections.singleton(registration)); - } - - private Saml2MetadataResponse responseByIterable(HttpServletRequest request, - Iterable registrations) { - Map results = new LinkedHashMap<>(); - for (RelyingPartyRegistration registration : registrations) { - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); - String entityId = uriResolver.resolve(registration.getEntityId()); - results.computeIfAbsent(entityId, (e) -> { - String ssoLocation = uriResolver.resolve(registration.getAssertionConsumerServiceLocation()); - String sloLocation = uriResolver.resolve(registration.getSingleLogoutServiceLocation()); - String sloResponseLocation = uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation()); - return registration.mutate() - .entityId(entityId) - .assertionConsumerServiceLocation(ssoLocation) - .singleLogoutServiceLocation(sloLocation) - .singleLogoutServiceResponseLocation(sloResponseLocation) - .build(); - }); - } - String metadata = this.metadata.resolve(results.values()); - String value = (results.size() == 1) ? results.values().iterator().next().getRegistrationId() - : UUID.randomUUID().toString(); - String fileName = this.filename.replace("{registrationId}", value); - try { - String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()); - return new Saml2MetadataResponse(metadata, encodedFileName); - } - catch (UnsupportedEncodingException ex) { - throw new Saml2Exception(ex); - } - } - - /** - * Use this {@link RequestMatcher} to identity which requests to generate metadata - * for. By default, matches {@code /saml2/metadata}, - * {@code /saml2/metadata/{registrationId}}, {@code /saml2/service-provider-metadata}, - * and {@code /saml2/service-provider-metadata/{registrationId}} - * @param requestMatcher the {@link RequestMatcher} to use - */ - public void setRequestMatcher(RequestMatcher requestMatcher) { - Assert.notNull(requestMatcher, "requestMatcher cannot be empty"); - this.matcher = requestMatcher; - } - - /** - * Sets the metadata filename template. If it contains the {@code {registrationId}} - * placeholder, it will be resolved as a random UUID if there are multiple - * {@link RelyingPartyRegistration}s. Otherwise, it will be replaced by the - * {@link RelyingPartyRegistration}'s id. - * - *

      - * The default value is {@code saml-{registrationId}-metadata.xml} - * @param metadataFilename metadata filename, must contain a {registrationId} - */ - public void setMetadataFilename(String metadataFilename) { - Assert.hasText(metadataFilename, "metadataFilename cannot be empty"); - this.filename = metadataFilename; - } - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResolver.java index c8fa5c60f4..999a124771 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResolver.java @@ -35,8 +35,4 @@ public interface Saml2MetadataResolver { */ String resolve(RelyingPartyRegistration relyingPartyRegistration); - default String resolve(Iterable relyingPartyRegistrations) { - return resolve(relyingPartyRegistrations.iterator().next()); - } - } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResponse.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResponse.java deleted file mode 100644 index 508430d735..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResponse.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2002-2023 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.saml2.provider.service.metadata; - -public class Saml2MetadataResponse { - - private final String metadata; - - private final String fileName; - - public Saml2MetadataResponse(String metadata, String fileName) { - this.metadata = metadata; - this.fileName = fileName; - } - - public String getMetadata() { - return this.metadata; - } - - public String getFileName() { - return this.fileName; - } - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResponseResolver.java deleted file mode 100644 index 2c3fdcf4a2..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2MetadataResponseResolver.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2002-2023 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.saml2.provider.service.metadata; - -import jakarta.servlet.http.HttpServletRequest; - -/** - * Resolves Relying Party SAML 2.0 Metadata given details from the - * {@link HttpServletRequest}. - * - * @author Josh Cummings - * @since 6.1 - */ -public interface Saml2MetadataResponseResolver { - - /** - * Construct and serialize a relying party's SAML 2.0 metadata based on the given - * {@link HttpServletRequest} - * @param request the HTTP request - * @return a {@link Saml2MetadataResponse} instance - */ - Saml2MetadataResponse resolve(HttpServletRequest request); - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java index 738f00952f..ce02633177 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java @@ -21,19 +21,11 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import org.springframework.util.Assert; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; /** - * An in-memory implementation of {@link RelyingPartyRegistrationRepository}. Also - * implements {@link Iterable} to simplify the default login page. - * - * @author Filip Hanik - * @author Josh Cummings * @since 5.2 */ public class InMemoryRelyingPartyRegistrationRepository @@ -41,8 +33,6 @@ public class InMemoryRelyingPartyRegistrationRepository private final Map byRegistrationId; - private final Map> byAssertingPartyEntityId; - public InMemoryRelyingPartyRegistrationRepository(RelyingPartyRegistration... registrations) { this(Arrays.asList(registrations)); } @@ -50,7 +40,6 @@ public class InMemoryRelyingPartyRegistrationRepository public InMemoryRelyingPartyRegistrationRepository(Collection registrations) { Assert.notEmpty(registrations, "registrations cannot be empty"); this.byRegistrationId = createMappingToIdentityProvider(registrations); - this.byAssertingPartyEntityId = createMappingByAssertingPartyEntityId(registrations); } private static Map createMappingToIdentityProvider( @@ -66,32 +55,11 @@ public class InMemoryRelyingPartyRegistrationRepository return Collections.unmodifiableMap(result); } - private static Map> createMappingByAssertingPartyEntityId( - Collection rps) { - MultiValueMap result = new LinkedMultiValueMap<>(); - for (RelyingPartyRegistration rp : rps) { - result.add(rp.getAssertingPartyDetails().getEntityId(), rp); - } - return Collections.unmodifiableMap(result); - } - @Override public RelyingPartyRegistration findByRegistrationId(String id) { return this.byRegistrationId.get(id); } - @Override - public RelyingPartyRegistration findUniqueByAssertingPartyEntityId(String entityId) { - Collection registrations = this.byAssertingPartyEntityId.get(entityId); - if (registrations == null) { - return null; - } - if (registrations.size() > 1) { - return null; - } - return registrations.iterator().next(); - } - @Override public Iterator iterator() { return this.byRegistrationId.values().iterator(); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyDetails.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyDetails.java index 25d5738a6c..5a224ac868 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyDetails.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyDetails.java @@ -16,14 +16,8 @@ package org.springframework.security.saml2.provider.service.registration; -import java.util.Collection; -import java.util.List; -import java.util.function.Consumer; - import org.opensaml.saml.saml2.metadata.EntityDescriptor; -import org.springframework.security.saml2.core.Saml2X509Credential; - /** * A {@link RelyingPartyRegistration.AssertingPartyDetails} that contains * OpenSAML-specific members @@ -72,92 +66,12 @@ public final class OpenSamlAssertingPartyDetails extends RelyingPartyRegistratio */ public static final class Builder extends RelyingPartyRegistration.AssertingPartyDetails.Builder { - private EntityDescriptor descriptor; + private final EntityDescriptor descriptor; private Builder(EntityDescriptor descriptor) { this.descriptor = descriptor; } - /** - * {@inheritDoc} - */ - @Override - public Builder entityId(String entityId) { - return (Builder) super.entityId(entityId); - } - - /** - * {@inheritDoc} - */ - @Override - public Builder wantAuthnRequestsSigned(boolean wantAuthnRequestsSigned) { - return (Builder) super.wantAuthnRequestsSigned(wantAuthnRequestsSigned); - } - - /** - * {@inheritDoc} - */ - @Override - public Builder signingAlgorithms(Consumer> signingMethodAlgorithmsConsumer) { - return (Builder) super.signingAlgorithms(signingMethodAlgorithmsConsumer); - } - - /** - * {@inheritDoc} - */ - @Override - public Builder verificationX509Credentials(Consumer> credentialsConsumer) { - return (Builder) super.verificationX509Credentials(credentialsConsumer); - } - - /** - * {@inheritDoc} - */ - @Override - public Builder encryptionX509Credentials(Consumer> credentialsConsumer) { - return (Builder) super.encryptionX509Credentials(credentialsConsumer); - } - - /** - * {@inheritDoc} - */ - @Override - public Builder singleSignOnServiceLocation(String singleSignOnServiceLocation) { - return (Builder) super.singleSignOnServiceLocation(singleSignOnServiceLocation); - } - - /** - * {@inheritDoc} - */ - @Override - public Builder singleSignOnServiceBinding(Saml2MessageBinding singleSignOnServiceBinding) { - return (Builder) super.singleSignOnServiceBinding(singleSignOnServiceBinding); - } - - /** - * {@inheritDoc} - */ - @Override - public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) { - return (Builder) super.singleLogoutServiceLocation(singleLogoutServiceLocation); - } - - /** - * {@inheritDoc} - */ - @Override - public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation) { - return (Builder) super.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation); - } - - /** - * {@inheritDoc} - */ - @Override - public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) { - return (Builder) super.singleLogoutServiceBinding(singleLogoutServiceBinding); - } - /** * Build an * {@link org.springframework.security.saml2.provider.service.registration.OpenSamlAssertingPartyDetails} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverter.java new file mode 100644 index 0000000000..53c06aa139 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverter.java @@ -0,0 +1,222 @@ +/* + * Copyright 2002-2023 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.saml2.provider.service.registration; + +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import net.shibboleth.utilities.java.support.xml.ParserPool; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.io.Unmarshaller; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.ext.saml2alg.SigningMethod; +import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml.saml2.metadata.Extensions; +import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; +import org.opensaml.saml.saml2.metadata.KeyDescriptor; +import org.opensaml.saml.saml2.metadata.SingleLogoutService; +import org.opensaml.saml.saml2.metadata.SingleSignOnService; +import org.opensaml.security.credential.UsageType; +import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.Saml2X509Credential; + +class OpenSamlMetadataAssertingPartyDetailsConverter { + + static { + OpenSamlInitializationService.initialize(); + } + + private final XMLObjectProviderRegistry registry; + + private final ParserPool parserPool; + + /** + * Creates a {@link OpenSamlMetadataAssertingPartyDetailsConverter} + */ + OpenSamlMetadataAssertingPartyDetailsConverter() { + this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class); + this.parserPool = this.registry.getParserPool(); + } + + Collection convert(InputStream inputStream) { + List builders = new ArrayList<>(); + XMLObject xmlObject = xmlObject(inputStream); + if (xmlObject instanceof EntitiesDescriptor) { + EntitiesDescriptor descriptors = (EntitiesDescriptor) xmlObject; + for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) { + if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) { + builders.add(convert(descriptor)); + } + } + if (builders.isEmpty()) { + throw new Saml2Exception("Metadata contains no IDPSSODescriptor elements"); + } + return builders; + } + if (xmlObject instanceof EntityDescriptor) { + EntityDescriptor descriptor = (EntityDescriptor) xmlObject; + return Arrays.asList(convert(descriptor)); + } + throw new Saml2Exception("Unsupported element of type " + xmlObject.getClass()); + } + + RelyingPartyRegistration.AssertingPartyDetails.Builder convert(EntityDescriptor descriptor) { + IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); + if (idpssoDescriptor == null) { + throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element"); + } + List verification = new ArrayList<>(); + List encryption = new ArrayList<>(); + for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) { + if (keyDescriptor.getUse().equals(UsageType.SIGNING)) { + List certificates = certificates(keyDescriptor); + for (X509Certificate certificate : certificates) { + verification.add(Saml2X509Credential.verification(certificate)); + } + } + if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) { + List certificates = certificates(keyDescriptor); + for (X509Certificate certificate : certificates) { + encryption.add(Saml2X509Credential.encryption(certificate)); + } + } + if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) { + List certificates = certificates(keyDescriptor); + for (X509Certificate certificate : certificates) { + verification.add(Saml2X509Credential.verification(certificate)); + encryption.add(Saml2X509Credential.encryption(certificate)); + } + } + } + if (verification.isEmpty()) { + throw new Saml2Exception( + "Metadata response is missing verification certificates, necessary for verifying SAML assertions"); + } + RelyingPartyRegistration.AssertingPartyDetails.Builder party = OpenSamlAssertingPartyDetails + .withEntityDescriptor(descriptor) + .entityId(descriptor.getEntityID()) + .wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned())) + .verificationX509Credentials((c) -> c.addAll(verification)) + .encryptionX509Credentials((c) -> c.addAll(encryption)); + List signingMethods = signingMethods(idpssoDescriptor); + for (SigningMethod method : signingMethods) { + party.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm())); + } + if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) { + throw new Saml2Exception( + "Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests"); + } + for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) { + Saml2MessageBinding binding; + if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) { + binding = Saml2MessageBinding.POST; + } + else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) { + binding = Saml2MessageBinding.REDIRECT; + } + else { + continue; + } + party.singleSignOnServiceLocation(singleSignOnService.getLocation()).singleSignOnServiceBinding(binding); + break; + } + for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) { + Saml2MessageBinding binding; + if (singleLogoutService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) { + binding = Saml2MessageBinding.POST; + } + else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) { + binding = Saml2MessageBinding.REDIRECT; + } + else { + continue; + } + String responseLocation = (singleLogoutService.getResponseLocation() == null) + ? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation(); + party.singleLogoutServiceLocation(singleLogoutService.getLocation()) + .singleLogoutServiceResponseLocation(responseLocation) + .singleLogoutServiceBinding(binding); + break; + } + return party; + } + + private List certificates(KeyDescriptor keyDescriptor) { + try { + return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo()); + } + catch (CertificateException ex) { + throw new Saml2Exception(ex); + } + } + + private List signingMethods(IDPSSODescriptor idpssoDescriptor) { + Extensions extensions = idpssoDescriptor.getExtensions(); + List result = signingMethods(extensions); + if (!result.isEmpty()) { + return result; + } + EntityDescriptor descriptor = (EntityDescriptor) idpssoDescriptor.getParent(); + extensions = descriptor.getExtensions(); + return signingMethods(extensions); + } + + private XMLObject xmlObject(InputStream inputStream) { + Document document = document(inputStream); + Element element = document.getDocumentElement(); + Unmarshaller unmarshaller = this.registry.getUnmarshallerFactory().getUnmarshaller(element); + if (unmarshaller == null) { + throw new Saml2Exception("Unsupported element of type " + element.getTagName()); + } + try { + return unmarshaller.unmarshall(element); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + private Document document(InputStream inputStream) { + try { + return this.parserPool.parse(inputStream); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + private List signingMethods(Extensions extensions) { + if (extensions != null) { + return (List) extensions.getUnknownXMLObjects(SigningMethod.DEFAULT_ELEMENT_NAME); + } + return new ArrayList<>(); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java index a3ab016d0e..882707c4f9 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverter.java @@ -17,212 +17,19 @@ package org.springframework.security.saml2.provider.service.registration; import java.io.InputStream; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; -import java.util.List; - -import net.shibboleth.utilities.java.support.xml.ParserPool; -import org.opensaml.core.config.ConfigurationService; -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.config.XMLObjectProviderRegistry; -import org.opensaml.core.xml.io.Unmarshaller; -import org.opensaml.saml.common.xml.SAMLConstants; -import org.opensaml.saml.ext.saml2alg.SigningMethod; -import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; -import org.opensaml.saml.saml2.metadata.EntityDescriptor; -import org.opensaml.saml.saml2.metadata.Extensions; -import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; -import org.opensaml.saml.saml2.metadata.KeyDescriptor; -import org.opensaml.saml.saml2.metadata.SingleLogoutService; -import org.opensaml.saml.saml2.metadata.SingleSignOnService; -import org.opensaml.security.credential.UsageType; -import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.core.OpenSamlInitializationService; -import org.springframework.security.saml2.core.Saml2X509Credential; class OpenSamlMetadataRelyingPartyRegistrationConverter { - static { - OpenSamlInitializationService.initialize(); - } + private final OpenSamlMetadataAssertingPartyDetailsConverter converter = new OpenSamlMetadataAssertingPartyDetailsConverter(); - private final XMLObjectProviderRegistry registry; - - private final ParserPool parserPool; - - /** - * Creates a {@link OpenSamlMetadataRelyingPartyRegistrationConverter} - */ - OpenSamlMetadataRelyingPartyRegistrationConverter() { - this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class); - this.parserPool = this.registry.getParserPool(); - } - - OpenSamlRelyingPartyRegistration.Builder convert(EntityDescriptor descriptor) { - IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); - if (idpssoDescriptor == null) { - throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element"); + Collection convert(InputStream source) { + Collection builders = new ArrayList<>(); + for (RelyingPartyRegistration.AssertingPartyDetails.Builder builder : this.converter.convert(source)) { + builders.add(new RelyingPartyRegistration.Builder(builder)); } - List verification = new ArrayList<>(); - List encryption = new ArrayList<>(); - for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) { - if (keyDescriptor.getUse().equals(UsageType.SIGNING)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - verification.add(Saml2X509Credential.verification(certificate)); - } - } - if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - encryption.add(Saml2X509Credential.encryption(certificate)); - } - } - if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) { - List certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - verification.add(Saml2X509Credential.verification(certificate)); - encryption.add(Saml2X509Credential.encryption(certificate)); - } - } - } - if (verification.isEmpty()) { - throw new Saml2Exception( - "Metadata response is missing verification certificates, necessary for verifying SAML assertions"); - } - OpenSamlRelyingPartyRegistration.Builder builder = OpenSamlRelyingPartyRegistration - .withAssertingPartyEntityDescriptor(descriptor) - .assertingPartyDetails((party) -> party.entityId(descriptor.getEntityID()) - .wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned())) - .verificationX509Credentials((c) -> c.addAll(verification)) - .encryptionX509Credentials((c) -> c.addAll(encryption))); - - List signingMethods = signingMethods(idpssoDescriptor); - for (SigningMethod method : signingMethods) { - builder.assertingPartyDetails( - (party) -> party.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm()))); - } - if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) { - throw new Saml2Exception( - "Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests"); - } - for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) { - Saml2MessageBinding binding; - if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) { - binding = Saml2MessageBinding.POST; - } - else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) { - binding = Saml2MessageBinding.REDIRECT; - } - else { - continue; - } - builder - .assertingPartyDetails((party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation()) - .singleSignOnServiceBinding(binding)); - break; - } - for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) { - Saml2MessageBinding binding; - if (singleLogoutService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) { - binding = Saml2MessageBinding.POST; - } - else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) { - binding = Saml2MessageBinding.REDIRECT; - } - else { - continue; - } - String responseLocation = (singleLogoutService.getResponseLocation() == null) - ? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation(); - builder - .assertingPartyDetails((party) -> party.singleLogoutServiceLocation(singleLogoutService.getLocation()) - .singleLogoutServiceResponseLocation(responseLocation) - .singleLogoutServiceBinding(binding)); - break; - } - - return builder; - } - - Collection convert(InputStream inputStream) { - List builders = new ArrayList<>(); - XMLObject xmlObject = xmlObject(inputStream); - if (xmlObject instanceof EntitiesDescriptor) { - EntitiesDescriptor descriptors = (EntitiesDescriptor) xmlObject; - for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) { - if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) { - builders.add(convert(descriptor)); - } - } - if (builders.isEmpty()) { - throw new Saml2Exception("Metadata contains no IDPSSODescriptor elements"); - } - return builders; - } - if (xmlObject instanceof EntityDescriptor) { - EntityDescriptor descriptor = (EntityDescriptor) xmlObject; - return Arrays.asList(convert(descriptor)); - } - throw new Saml2Exception("Unsupported element of type " + xmlObject.getClass()); - } - - private List certificates(KeyDescriptor keyDescriptor) { - try { - return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo()); - } - catch (CertificateException ex) { - throw new Saml2Exception(ex); - } - } - - private List signingMethods(IDPSSODescriptor idpssoDescriptor) { - Extensions extensions = idpssoDescriptor.getExtensions(); - List result = signingMethods(extensions); - if (!result.isEmpty()) { - return result; - } - EntityDescriptor descriptor = (EntityDescriptor) idpssoDescriptor.getParent(); - extensions = descriptor.getExtensions(); - return signingMethods(extensions); - } - - private XMLObject xmlObject(InputStream inputStream) { - Document document = document(inputStream); - Element element = document.getDocumentElement(); - Unmarshaller unmarshaller = this.registry.getUnmarshallerFactory().getUnmarshaller(element); - if (unmarshaller == null) { - throw new Saml2Exception("Unsupported element of type " + element.getTagName()); - } - try { - return unmarshaller.unmarshall(element); - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - private Document document(InputStream inputStream) { - try { - return this.parserPool.parse(inputStream); - } - catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - private List signingMethods(Extensions extensions) { - if (extensions != null) { - return (List) extensions.getUnknownXMLObjects(SigningMethod.DEFAULT_ELEMENT_NAME); - } - return new ArrayList<>(); + return builders; } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java deleted file mode 100644 index ce9061ad4a..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2002-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.saml2.provider.service.registration; - -import java.util.Collection; -import java.util.function.Consumer; - -import org.opensaml.saml.saml2.metadata.EntityDescriptor; - -import org.springframework.security.saml2.core.Saml2X509Credential; - -/** - * An OpenSAML implementation of {@link RelyingPartyRegistration} that contains OpenSAML - * objects like {@link EntityDescriptor}. - * - * @author Josh Cummings - * @since 6.1 - */ -public final class OpenSamlRelyingPartyRegistration extends RelyingPartyRegistration { - - OpenSamlRelyingPartyRegistration(RelyingPartyRegistration registration) { - super(registration.getRegistrationId(), registration.getEntityId(), - registration.getAssertionConsumerServiceLocation(), registration.getAssertionConsumerServiceBinding(), - registration.getSingleLogoutServiceLocation(), registration.getSingleLogoutServiceResponseLocation(), - registration.getSingleLogoutServiceBindings(), registration.getAssertingPartyDetails(), - registration.getNameIdFormat(), registration.isAuthnRequestsSigned(), - registration.getDecryptionX509Credentials(), registration.getSigningX509Credentials()); - } - - /** - * {@inheritDoc} - */ - @Override - public OpenSamlRelyingPartyRegistration.Builder mutate() { - OpenSamlAssertingPartyDetails party = getAssertingPartyDetails(); - return withAssertingPartyEntityDescriptor(party.getEntityDescriptor()).registrationId(getRegistrationId()) - .entityId(getEntityId()) - .signingX509Credentials((c) -> c.addAll(getSigningX509Credentials())) - .decryptionX509Credentials((c) -> c.addAll(getDecryptionX509Credentials())) - .assertionConsumerServiceLocation(getAssertionConsumerServiceLocation()) - .assertionConsumerServiceBinding(getAssertionConsumerServiceBinding()) - .singleLogoutServiceLocation(getSingleLogoutServiceLocation()) - .singleLogoutServiceResponseLocation(getSingleLogoutServiceResponseLocation()) - .singleLogoutServiceBindings((c) -> c.addAll(getSingleLogoutServiceBindings())) - .nameIdFormat(getNameIdFormat()) - .authnRequestsSigned(isAuthnRequestsSigned()) - .assertingPartyDetails((assertingParty) -> ((OpenSamlAssertingPartyDetails.Builder) assertingParty) - .entityId(party.getEntityId()) - .wantAuthnRequestsSigned(party.getWantAuthnRequestsSigned()) - .signingAlgorithms((algorithms) -> algorithms.addAll(party.getSigningAlgorithms())) - .verificationX509Credentials((c) -> c.addAll(party.getVerificationX509Credentials())) - .encryptionX509Credentials((c) -> c.addAll(party.getEncryptionX509Credentials())) - .singleSignOnServiceLocation(party.getSingleSignOnServiceLocation()) - .singleSignOnServiceBinding(party.getSingleSignOnServiceBinding()) - .singleLogoutServiceLocation(party.getSingleLogoutServiceLocation()) - .singleLogoutServiceResponseLocation(party.getSingleLogoutServiceResponseLocation()) - .singleLogoutServiceBinding(party.getSingleLogoutServiceBinding())); - } - - /** - * {@inheritDoc} - */ - @Override - public OpenSamlAssertingPartyDetails getAssertingPartyDetails() { - return (OpenSamlAssertingPartyDetails) super.getAssertingPartyDetails(); - } - - /** - * Create a {@link Builder} from an entity descriptor - * @param entityDescriptor the asserting party's {@link EntityDescriptor} - * @return an {@link Builder} - */ - public static OpenSamlRelyingPartyRegistration.Builder withAssertingPartyEntityDescriptor( - EntityDescriptor entityDescriptor) { - return new Builder(entityDescriptor); - } - - /** - * An OpenSAML version of - * {@link org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails.Builder} - * that contains the underlying {@link EntityDescriptor} - */ - public static final class Builder extends RelyingPartyRegistration.Builder { - - private Builder(EntityDescriptor entityDescriptor) { - super(entityDescriptor.getEntityID(), OpenSamlAssertingPartyDetails.withEntityDescriptor(entityDescriptor)); - } - - @Override - public Builder registrationId(String id) { - return (Builder) super.registrationId(id); - } - - public Builder entityId(String entityId) { - return (Builder) super.entityId(entityId); - } - - public Builder signingX509Credentials(Consumer> credentialsConsumer) { - return (Builder) super.signingX509Credentials(credentialsConsumer); - } - - @Override - public Builder decryptionX509Credentials(Consumer> credentialsConsumer) { - return (Builder) super.decryptionX509Credentials(credentialsConsumer); - } - - @Override - public Builder assertionConsumerServiceLocation(String assertionConsumerServiceLocation) { - return (Builder) super.assertionConsumerServiceLocation(assertionConsumerServiceLocation); - } - - @Override - public Builder assertionConsumerServiceBinding(Saml2MessageBinding assertionConsumerServiceBinding) { - return (Builder) super.assertionConsumerServiceBinding(assertionConsumerServiceBinding); - } - - @Override - public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) { - return singleLogoutServiceBindings((saml2MessageBindings) -> { - saml2MessageBindings.clear(); - saml2MessageBindings.add(singleLogoutServiceBinding); - }); - } - - @Override - public Builder singleLogoutServiceBindings(Consumer> bindingsConsumer) { - return (Builder) super.singleLogoutServiceBindings(bindingsConsumer); - } - - @Override - public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) { - return (Builder) super.singleLogoutServiceLocation(singleLogoutServiceLocation); - } - - public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation) { - return (Builder) super.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation); - } - - @Override - public Builder nameIdFormat(String nameIdFormat) { - return (Builder) super.nameIdFormat(nameIdFormat); - } - - @Override - public Builder authnRequestsSigned(Boolean authnRequestsSigned) { - return (Builder) super.authnRequestsSigned(authnRequestsSigned); - } - - @Override - public Builder assertingPartyDetails(Consumer assertingPartyDetails) { - return (Builder) super.assertingPartyDetails(assertingPartyDetails); - } - - /** - * Build an {@link OpenSamlRelyingPartyRegistration} - * {@link org.springframework.security.saml2.provider.service.registration.OpenSamlRelyingPartyRegistration} - * @return an {@link OpenSamlRelyingPartyRegistration} - */ - @Override - public OpenSamlRelyingPartyRegistration build() { - return new OpenSamlRelyingPartyRegistration(super.build()); - } - - } - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java index 3d4069ab8f..1996c6246d 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter.java @@ -62,13 +62,13 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter OpenSamlInitializationService.initialize(); } - private final OpenSamlMetadataRelyingPartyRegistrationConverter converter; + private final OpenSamlMetadataAssertingPartyDetailsConverter converter; /** * Creates a {@link OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter} */ public OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter() { - this.converter = new OpenSamlMetadataRelyingPartyRegistrationConverter(); + this.converter = new OpenSamlMetadataAssertingPartyDetailsConverter(); } @Override @@ -89,7 +89,7 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter @Override public RelyingPartyRegistration.Builder read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - return this.converter.convert(inputMessage.getBody()).iterator().next(); + return new RelyingPartyRegistration.Builder(this.converter.convert(inputMessage.getBody()).iterator().next()); } @Override diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java index 9e6f1b7533..8f36df594c 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java @@ -26,6 +26,7 @@ import java.util.function.Consumer; import org.opensaml.xmlsec.signature.support.SignatureConstants; +import org.springframework.core.convert.converter.Converter; import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -68,7 +69,7 @@ import org.springframework.util.CollectionUtils; * @author Josh Cummings * @since 5.2 */ -public class RelyingPartyRegistration { +public final class RelyingPartyRegistration { private final String registrationId; @@ -86,18 +87,16 @@ public class RelyingPartyRegistration { private final String nameIdFormat; - private final boolean authnRequestsSigned; - private final AssertingPartyDetails assertingPartyDetails; private final Collection decryptionX509Credentials; private final Collection signingX509Credentials; - protected RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation, + private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation, Saml2MessageBinding assertionConsumerServiceBinding, String singleLogoutServiceLocation, String singleLogoutServiceResponseLocation, Collection singleLogoutServiceBindings, - AssertingPartyDetails assertingPartyDetails, String nameIdFormat, boolean authnRequestsSigned, + AssertingPartyDetails assertingPartyDetails, String nameIdFormat, Collection decryptionX509Credentials, Collection signingX509Credentials) { Assert.hasText(registrationId, "registrationId cannot be empty"); @@ -126,42 +125,11 @@ public class RelyingPartyRegistration { this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation; this.singleLogoutServiceBindings = Collections.unmodifiableList(new LinkedList<>(singleLogoutServiceBindings)); this.nameIdFormat = nameIdFormat; - this.authnRequestsSigned = authnRequestsSigned; this.assertingPartyDetails = assertingPartyDetails; this.decryptionX509Credentials = Collections.unmodifiableList(new LinkedList<>(decryptionX509Credentials)); this.signingX509Credentials = Collections.unmodifiableList(new LinkedList<>(signingX509Credentials)); } - /** - * Copy the properties in this {@link RelyingPartyRegistration} into a {@link Builder} - * @return a {@link Builder} based off of the properties in this - * {@link RelyingPartyRegistration} - * @since 6.1 - */ - public Builder mutate() { - AssertingPartyDetails party = this.assertingPartyDetails; - return withRegistrationId(this.registrationId).entityId(this.entityId) - .signingX509Credentials((c) -> c.addAll(this.signingX509Credentials)) - .decryptionX509Credentials((c) -> c.addAll(this.decryptionX509Credentials)) - .assertionConsumerServiceLocation(this.assertionConsumerServiceLocation) - .assertionConsumerServiceBinding(this.assertionConsumerServiceBinding) - .singleLogoutServiceLocation(this.singleLogoutServiceLocation) - .singleLogoutServiceResponseLocation(this.singleLogoutServiceResponseLocation) - .singleLogoutServiceBindings((c) -> c.addAll(this.singleLogoutServiceBindings)) - .nameIdFormat(this.nameIdFormat) - .authnRequestsSigned(this.authnRequestsSigned) - .assertingPartyDetails((assertingParty) -> assertingParty.entityId(party.getEntityId()) - .wantAuthnRequestsSigned(party.getWantAuthnRequestsSigned()) - .signingAlgorithms((algorithms) -> algorithms.addAll(party.getSigningAlgorithms())) - .verificationX509Credentials((c) -> c.addAll(party.getVerificationX509Credentials())) - .encryptionX509Credentials((c) -> c.addAll(party.getEncryptionX509Credentials())) - .singleSignOnServiceLocation(party.getSingleSignOnServiceLocation()) - .singleSignOnServiceBinding(party.getSingleSignOnServiceBinding()) - .singleLogoutServiceLocation(party.getSingleLogoutServiceLocation()) - .singleLogoutServiceResponseLocation(party.getSingleLogoutServiceResponseLocation()) - .singleLogoutServiceBinding(party.getSingleLogoutServiceBinding())); - } - /** * Get the unique registration id for this RP/AP pair * @return the unique registration id for this RP/AP pair @@ -285,23 +253,6 @@ public class RelyingPartyRegistration { return this.nameIdFormat; } - /** - * Get the - * AuthnRequestsSigned setting. If {@code true}, the relying party will sign all - * AuthnRequests, regardless of asserting party preference. - * - *

      - * Note that Spring Security will sign the request if either - * {@link #isAuthnRequestsSigned()} is {@code true} or - * {@link AssertingPartyDetails#getWantAuthnRequestsSigned()} is {@code true}. - * @return the relying-party preference - * @since 6.1 - */ - public boolean isAuthnRequestsSigned() { - return this.authnRequestsSigned; - } - /** * Get the {@link Collection} of decryption {@link Saml2X509Credential}s associated * with this relying party @@ -341,7 +292,7 @@ public class RelyingPartyRegistration { */ public static Builder withRegistrationId(String registrationId) { Assert.hasText(registrationId, "registrationId cannot be empty"); - return new Builder(registrationId, new AssertingPartyDetails.Builder()); + return new Builder(registrationId); } public static Builder withAssertingPartyDetails(AssertingPartyDetails assertingPartyDetails) { @@ -364,9 +315,7 @@ public class RelyingPartyRegistration { * object * @param registration the {@code RelyingPartyRegistration} * @return {@code Builder} to create a {@code RelyingPartyRegistration} object - * @deprecated Use {@link #mutate()} instead */ - @Deprecated(forRemoval = true, since = "6.1") public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) { Assert.notNull(registration, "registration cannot be null"); return withRegistrationId(registration.getRegistrationId()).entityId(registration.getEntityId()) @@ -378,7 +327,6 @@ public class RelyingPartyRegistration { .singleLogoutServiceResponseLocation(registration.getSingleLogoutServiceResponseLocation()) .singleLogoutServiceBindings((c) -> c.addAll(registration.getSingleLogoutServiceBindings())) .nameIdFormat(registration.getNameIdFormat()) - .authnRequestsSigned(registration.isAuthnRequestsSigned()) .assertingPartyDetails((assertingParty) -> assertingParty .entityId(registration.getAssertingPartyDetails().getEntityId()) .wantAuthnRequestsSigned(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) @@ -784,9 +732,9 @@ public class RelyingPartyRegistration { } - public static class Builder { + public static final class Builder { - private String registrationId; + private Converter registrationId = AssertingPartyDetails::getEntityId; private String entityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}"; @@ -806,13 +754,15 @@ public class RelyingPartyRegistration { private String nameIdFormat = null; - private boolean authnRequestsSigned = false; - private AssertingPartyDetails.Builder assertingPartyDetailsBuilder; - protected Builder(String registrationId, AssertingPartyDetails.Builder assertingPartyDetailsBuilder) { - this.registrationId = registrationId; - this.assertingPartyDetailsBuilder = assertingPartyDetailsBuilder; + private Builder(String registrationId) { + this.registrationId = (party) -> registrationId; + this.assertingPartyDetailsBuilder = new AssertingPartyDetails.Builder(); + } + + Builder(AssertingPartyDetails.Builder builder) { + this.assertingPartyDetailsBuilder = builder; } /** @@ -821,7 +771,7 @@ public class RelyingPartyRegistration { * @return this object */ public Builder registrationId(String id) { - this.registrationId = id; + this.registrationId = (party) -> id; return this; } @@ -994,24 +944,6 @@ public class RelyingPartyRegistration { return this; } - /** - * Set the - * AuthnRequestsSigned setting. If {@code true}, the relying party will sign - * all AuthnRequests, 301 asserting party preference. - * - *

      - * Note that Spring Security will sign the request if either - * {@link #isAuthnRequestsSigned()} is {@code true} or - * {@link AssertingPartyDetails#getWantAuthnRequestsSigned()} is {@code true}. - * @return the {@link Builder} for further configuration - * @since 6.1 - */ - public Builder authnRequestsSigned(Boolean authnRequestsSigned) { - this.authnRequestsSigned = authnRequestsSigned; - return this; - } - /** * Apply this {@link Consumer} to further configure the Asserting Party details * @param assertingPartyDetails The {@link Consumer} to apply @@ -1038,11 +970,11 @@ public class RelyingPartyRegistration { } AssertingPartyDetails party = this.assertingPartyDetailsBuilder.build(); - return new RelyingPartyRegistration(this.registrationId, this.entityId, - this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding, - this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation, - this.singleLogoutServiceBindings, party, this.nameIdFormat, this.authnRequestsSigned, - this.decryptionX509Credentials, this.signingX509Credentials); + String registrationId = this.registrationId.convert(party); + return new RelyingPartyRegistration(registrationId, this.entityId, this.assertionConsumerServiceLocation, + this.assertionConsumerServiceBinding, this.singleLogoutServiceLocation, + this.singleLogoutServiceResponseLocation, this.singleLogoutServiceBindings, party, + this.nameIdFormat, this.decryptionX509Credentials, this.signingX509Credentials); } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.java index 691d82f009..1c681d92a3 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.java @@ -20,7 +20,6 @@ package org.springframework.security.saml2.provider.service.registration; * A repository for {@link RelyingPartyRegistration}s * * @author Filip Hanik - * @author Josh Cummings * @since 5.2 */ public interface RelyingPartyRegistrationRepository { @@ -33,16 +32,4 @@ public interface RelyingPartyRegistrationRepository { */ RelyingPartyRegistration findByRegistrationId(String registrationId); - /** - * Returns the unique relying party registration associated with the asserting party's - * {@code entityId} or {@code null} if there is no unique match. - * @param entityId the asserting party's entity id - * @return the unique {@link RelyingPartyRegistration} associated the given asserting - * party; {@code null} of there is no unique match asserting party - * @since 6.1 - */ - default RelyingPartyRegistration findUniqueByAssertingPartyEntityId(String entityId) { - return findByRegistrationId(entityId); - } - } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java index c2f71faa2c..f618488771 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java @@ -16,6 +16,10 @@ package org.springframework.security.saml2.provider.service.web; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -23,10 +27,13 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.convert.converter.Converter; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver; +import org.springframework.security.web.util.UrlUtils; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; /** * A {@link Converter} that resolves a {@link RelyingPartyRegistration} by extracting the @@ -41,6 +48,8 @@ public final class DefaultRelyingPartyRegistrationResolver private Log logger = LogFactory.getLog(getClass()); + private static final char PATH_DELIMITER = '/'; + private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; private final RequestMatcher registrationRequestMatcher = new AntPathRequestMatcher("/**/{registrationId}"); @@ -79,19 +88,68 @@ public final class DefaultRelyingPartyRegistrationResolver } return null; } - RelyingPartyRegistration registration = this.relyingPartyRegistrationRepository + RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationRepository .findByRegistrationId(relyingPartyRegistrationId); - if (registration == null) { + if (relyingPartyRegistration == null) { return null; } - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); - return registration.mutate() - .entityId(uriResolver.resolve(registration.getEntityId())) - .assertionConsumerServiceLocation(uriResolver.resolve(registration.getAssertionConsumerServiceLocation())) - .singleLogoutServiceLocation(uriResolver.resolve(registration.getSingleLogoutServiceLocation())) - .singleLogoutServiceResponseLocation( - uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation())) + String applicationUri = getApplicationUri(request); + Function templateResolver = templateResolver(applicationUri, relyingPartyRegistration); + String relyingPartyEntityId = templateResolver.apply(relyingPartyRegistration.getEntityId()); + String assertionConsumerServiceLocation = templateResolver + .apply(relyingPartyRegistration.getAssertionConsumerServiceLocation()); + String singleLogoutServiceLocation = templateResolver + .apply(relyingPartyRegistration.getSingleLogoutServiceLocation()); + String singleLogoutServiceResponseLocation = templateResolver + .apply(relyingPartyRegistration.getSingleLogoutServiceResponseLocation()); + return RelyingPartyRegistration.withRelyingPartyRegistration(relyingPartyRegistration) + .entityId(relyingPartyEntityId) + .assertionConsumerServiceLocation(assertionConsumerServiceLocation) + .singleLogoutServiceLocation(singleLogoutServiceLocation) + .singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation) .build(); } + private Function templateResolver(String applicationUri, RelyingPartyRegistration relyingParty) { + return (template) -> resolveUrlTemplate(template, applicationUri, relyingParty); + } + + private static String resolveUrlTemplate(String template, String baseUrl, RelyingPartyRegistration relyingParty) { + if (template == null) { + return null; + } + String entityId = relyingParty.getAssertingPartyDetails().getEntityId(); + String registrationId = relyingParty.getRegistrationId(); + Map uriVariables = new HashMap<>(); + UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(baseUrl) + .replaceQuery(null) + .fragment(null) + .build(); + String scheme = uriComponents.getScheme(); + uriVariables.put("baseScheme", (scheme != null) ? scheme : ""); + String host = uriComponents.getHost(); + uriVariables.put("baseHost", (host != null) ? host : ""); + // following logic is based on HierarchicalUriComponents#toUriString() + int port = uriComponents.getPort(); + uriVariables.put("basePort", (port == -1) ? "" : ":" + port); + String path = uriComponents.getPath(); + if (StringUtils.hasLength(path) && path.charAt(0) != PATH_DELIMITER) { + path = PATH_DELIMITER + path; + } + uriVariables.put("basePath", (path != null) ? path : ""); + uriVariables.put("baseUrl", uriComponents.toUriString()); + uriVariables.put("entityId", StringUtils.hasText(entityId) ? entityId : ""); + uriVariables.put("registrationId", StringUtils.hasText(registrationId) ? registrationId : ""); + return UriComponentsBuilder.fromUriString(template).buildAndExpand(uriVariables).toUriString(); + } + + private static String getApplicationUri(HttpServletRequest request) { + UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) + .replacePath(request.getContextPath()) + .replaceQuery(null) + .fragment(null) + .build(); + return uriComponents.toUriString(); + } + } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/OpenSamlAuthenticationTokenConverter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/OpenSamlAuthenticationTokenConverter.java deleted file mode 100644 index 83f082284e..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/OpenSamlAuthenticationTokenConverter.java +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright 2002-2023 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.saml2.provider.service.web; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Base64; -import java.util.function.Function; -import java.util.zip.Inflater; -import java.util.zip.InflaterOutputStream; - -import jakarta.servlet.http.HttpServletRequest; -import net.shibboleth.utilities.java.support.xml.ParserPool; -import org.opensaml.core.config.ConfigurationService; -import org.opensaml.core.xml.config.XMLObjectProviderRegistry; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; -import org.opensaml.saml.saml2.core.Response; -import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import org.springframework.http.HttpMethod; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.core.OpenSamlInitializationService; -import org.springframework.security.saml2.core.Saml2Error; -import org.springframework.security.saml2.core.Saml2ErrorCodes; -import org.springframework.security.saml2.core.Saml2ParameterNames; -import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver; -import org.springframework.security.web.authentication.AuthenticationConverter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.OrRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.Assert; - -/** - * An {@link AuthenticationConverter} that generates a {@link Saml2AuthenticationToken} - * appropriate for authenticated a SAML 2.0 Assertion against an - * {@link org.springframework.security.authentication.AuthenticationManager}. - * - * @author Josh Cummings - * @since 6.1 - */ -public final class OpenSamlAuthenticationTokenConverter implements AuthenticationConverter { - - static { - OpenSamlInitializationService.initialize(); - } - - // MimeDecoder allows extra line-breaks as well as other non-alphabet values. - // This matches the behaviour of the commons-codec decoder. - private static final Base64.Decoder BASE64 = Base64.getMimeDecoder(); - - private static final Base64Checker BASE_64_CHECKER = new Base64Checker(); - - private final RelyingPartyRegistrationRepository registrations; - - private RequestMatcher requestMatcher = new OrRequestMatcher( - new AntPathRequestMatcher("/login/saml2/sso/{registrationId}"), - new AntPathRequestMatcher("/login/saml2/sso")); - - private final ParserPool parserPool; - - private final ResponseUnmarshaller unmarshaller; - - private Function loader; - - /** - * Constructs a {@link OpenSamlAuthenticationTokenConverter} given a repository for - * {@link RelyingPartyRegistration}s - * @param registrations the repository for {@link RelyingPartyRegistration}s - * {@link RelyingPartyRegistration}s - */ - public OpenSamlAuthenticationTokenConverter(RelyingPartyRegistrationRepository registrations) { - Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null"); - XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); - this.parserPool = registry.getParserPool(); - this.unmarshaller = (ResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory() - .getUnmarshaller(Response.DEFAULT_ELEMENT_NAME); - this.registrations = registrations; - this.loader = new HttpSessionSaml2AuthenticationRequestRepository()::loadAuthenticationRequest; - } - - /** - * Resolve an authentication request from the given {@link HttpServletRequest}. - * - *

      - * First uses the configured {@link RequestMatcher} to deduce whether an - * authentication request is being made and optionally for which - * {@code registrationId}. - * - *

      - * If there is an associated {@code }, then the - * {@code registrationId} is looked up and used. - * - *

      - * If a {@code registrationId} is found in the request, then it is looked up and used. - * In that case, if none is found a {@link Saml2AuthenticationException} is thrown. - * - *

      - * Finally, if no {@code registrationId} is found in the request, then the code - * attempts to resolve the {@link RelyingPartyRegistration} from the SAML Response's - * Issuer. - * @param request the HTTP request - * @return the {@link Saml2AuthenticationToken} authentication request - * @throws Saml2AuthenticationException if the {@link RequestMatcher} specifies a - * non-existent {@code registrationId} - */ - @Override - public Saml2AuthenticationToken convert(HttpServletRequest request) { - String serialized = request.getParameter(Saml2ParameterNames.SAML_RESPONSE); - if (serialized == null) { - return null; - } - RequestMatcher.MatchResult result = this.requestMatcher.matcher(request); - if (!result.isMatch()) { - return null; - } - Saml2AuthenticationToken token = tokenByAuthenticationRequest(request); - if (token == null) { - token = tokenByRegistrationId(request, result); - } - if (token == null) { - token = tokenByEntityId(request); - } - return token; - } - - private Saml2AuthenticationToken tokenByAuthenticationRequest(HttpServletRequest request) { - AbstractSaml2AuthenticationRequest authenticationRequest = loadAuthenticationRequest(request); - if (authenticationRequest == null) { - return null; - } - String registrationId = authenticationRequest.getRelyingPartyRegistrationId(); - RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId); - return tokenByRegistration(request, registration, authenticationRequest); - } - - private Saml2AuthenticationToken tokenByRegistrationId(HttpServletRequest request, - RequestMatcher.MatchResult result) { - String registrationId = result.getVariables().get("registrationId"); - if (registrationId == null) { - return null; - } - RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId); - return tokenByRegistration(request, registration, null); - } - - private Saml2AuthenticationToken tokenByEntityId(HttpServletRequest request) { - String serialized = request.getParameter(Saml2ParameterNames.SAML_RESPONSE); - String decoded = new String(samlDecode(serialized), StandardCharsets.UTF_8); - Response response = parse(decoded); - String issuer = response.getIssuer().getValue(); - RelyingPartyRegistration registration = this.registrations.findUniqueByAssertingPartyEntityId(issuer); - return tokenByRegistration(request, registration, null); - } - - private Saml2AuthenticationToken tokenByRegistration(HttpServletRequest request, - RelyingPartyRegistration registration, AbstractSaml2AuthenticationRequest authenticationRequest) { - if (registration == null) { - return null; - } - String serialized = request.getParameter(Saml2ParameterNames.SAML_RESPONSE); - String decoded = inflateIfRequired(request, samlDecode(serialized)); - UriResolver resolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); - registration = registration.mutate() - .entityId(resolver.resolve(registration.getEntityId())) - .assertionConsumerServiceLocation(resolver.resolve(registration.getAssertionConsumerServiceLocation())) - .build(); - return new Saml2AuthenticationToken(registration, decoded, authenticationRequest); - } - - /** - * Use the given {@link Saml2AuthenticationRequestRepository} to load authentication - * request. - * @param authenticationRequestRepository the - * {@link Saml2AuthenticationRequestRepository} to use - */ - public void setAuthenticationRequestRepository( - Saml2AuthenticationRequestRepository authenticationRequestRepository) { - Assert.notNull(authenticationRequestRepository, "authenticationRequestRepository cannot be null"); - this.loader = authenticationRequestRepository::loadAuthenticationRequest; - } - - /** - * Use the given {@link RequestMatcher} to match the request. - * @param requestMatcher the {@link RequestMatcher} to use - */ - public void setRequestMatcher(RequestMatcher requestMatcher) { - Assert.notNull(requestMatcher, "requestMatcher cannot be null"); - this.requestMatcher = requestMatcher; - } - - private AbstractSaml2AuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) { - return this.loader.apply(request); - } - - private String inflateIfRequired(HttpServletRequest request, byte[] b) { - if (HttpMethod.GET.matches(request.getMethod())) { - return samlInflate(b); - } - return new String(b, StandardCharsets.UTF_8); - } - - private byte[] samlDecode(String base64EncodedPayload) { - try { - BASE_64_CHECKER.checkAcceptable(base64EncodedPayload); - return BASE64.decode(base64EncodedPayload); - } - catch (Exception ex) { - throw new Saml2AuthenticationException( - new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, "Failed to decode SAMLResponse"), ex); - } - } - - private String samlInflate(byte[] b) { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - InflaterOutputStream inflaterOutputStream = new InflaterOutputStream(out, new Inflater(true)); - inflaterOutputStream.write(b); - inflaterOutputStream.finish(); - return out.toString(StandardCharsets.UTF_8.name()); - } - catch (Exception ex) { - throw new Saml2AuthenticationException( - new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, "Unable to inflate string"), ex); - } - } - - private Response parse(String request) throws Saml2Exception { - try { - Document document = this.parserPool - .parse(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8))); - Element element = document.getDocumentElement(); - return (Response) this.unmarshaller.unmarshall(element); - } - catch (Exception ex) { - throw new Saml2Exception("Failed to deserialize LogoutRequest", ex); - } - } - - static class Base64Checker { - - private static final int[] values = genValueMapping(); - - Base64Checker() { - - } - - private static int[] genValueMapping() { - byte[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" - .getBytes(StandardCharsets.ISO_8859_1); - - int[] values = new int[256]; - Arrays.fill(values, -1); - for (int i = 0; i < alphabet.length; i++) { - values[alphabet[i] & 0xff] = i; - } - return values; - } - - boolean isAcceptable(String s) { - int goodChars = 0; - int lastGoodCharVal = -1; - - // count number of characters from Base64 alphabet - for (int i = 0; i < s.length(); i++) { - int val = values[0xff & s.charAt(i)]; - if (val != -1) { - lastGoodCharVal = val; - goodChars++; - } - } - - // in cases of an incomplete final chunk, ensure the unused bits are zero - switch (goodChars % 4) { - case 0: - return true; - case 2: - return (lastGoodCharVal & 0b1111) == 0; - case 3: - return (lastGoodCharVal & 0b11) == 0; - default: - return false; - } - } - - void checkAcceptable(String ins) { - if (!isAcceptable(ins)) { - throw new IllegalArgumentException("Unaccepted Encoding"); - } - } - - } - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationPlaceholderResolvers.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationPlaceholderResolvers.java deleted file mode 100644 index 1161a029b7..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationPlaceholderResolvers.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2002-2023 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.saml2.provider.service.web; - -import java.util.HashMap; -import java.util.Map; - -import jakarta.servlet.http.HttpServletRequest; - -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.web.util.UrlUtils; -import org.springframework.util.StringUtils; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; - -/** - * A factory for creating placeholder resolvers for {@link RelyingPartyRegistration} - * templates. Supports {@code baseUrl}, {@code baseScheme}, {@code baseHost}, - * {@code basePort}, {@code basePath}, {@code registrationId}, - * {@code relyingPartyEntityId}, and {@code assertingPartyEntityId} - * - * @author Josh Cummings - * @since 6.1 - */ -public final class RelyingPartyRegistrationPlaceholderResolvers { - - private static final char PATH_DELIMITER = '/'; - - private RelyingPartyRegistrationPlaceholderResolvers() { - - } - - /** - * Create a resolver based on the given {@link HttpServletRequest}. Given the request, - * placeholders {@code baseUrl}, {@code baseScheme}, {@code baseHost}, - * {@code basePort}, and {@code basePath} are resolved. - * @param request the HTTP request - * @return a resolver that can resolve {@code baseUrl}, {@code baseScheme}, - * {@code baseHost}, {@code basePort}, and {@code basePath} placeholders - */ - public static UriResolver uriResolver(HttpServletRequest request) { - return new UriResolver(uriVariables(request)); - } - - /** - * Create a resolver based on the given {@link HttpServletRequest}. Given the request, - * placeholders {@code baseUrl}, {@code baseScheme}, {@code baseHost}, - * {@code basePort}, {@code basePath}, {@code registrationId}, - * {@code assertingPartyEntityId}, and {@code relyingPartyEntityId} are resolved. - * @param request the HTTP request - * @return a resolver that can resolve {@code baseUrl}, {@code baseScheme}, - * {@code baseHost}, {@code basePort}, {@code basePath}, {@code registrationId}, - * {@code relyingPartyEntityId}, and {@code assertingPartyEntityId} placeholders - */ - public static UriResolver uriResolver(HttpServletRequest request, RelyingPartyRegistration registration) { - String relyingPartyEntityId = registration.getEntityId(); - String assertingPartyEntityId = registration.getAssertingPartyDetails().getEntityId(); - String registrationId = registration.getRegistrationId(); - Map uriVariables = uriVariables(request); - uriVariables.put("relyingPartyEntityId", StringUtils.hasText(relyingPartyEntityId) ? relyingPartyEntityId : ""); - uriVariables.put("assertingPartyEntityId", - StringUtils.hasText(assertingPartyEntityId) ? assertingPartyEntityId : ""); - uriVariables.put("entityId", StringUtils.hasText(assertingPartyEntityId) ? assertingPartyEntityId : ""); - uriVariables.put("registrationId", StringUtils.hasText(registrationId) ? registrationId : ""); - return new UriResolver(uriVariables); - } - - private static Map uriVariables(HttpServletRequest request) { - String baseUrl = getApplicationUri(request); - Map uriVariables = new HashMap<>(); - UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(baseUrl) - .replaceQuery(null) - .fragment(null) - .build(); - String scheme = uriComponents.getScheme(); - uriVariables.put("baseScheme", (scheme != null) ? scheme : ""); - String host = uriComponents.getHost(); - uriVariables.put("baseHost", (host != null) ? host : ""); - // following logic is based on HierarchicalUriComponents#toUriString() - int port = uriComponents.getPort(); - uriVariables.put("basePort", (port == -1) ? "" : ":" + port); - String path = uriComponents.getPath(); - if (StringUtils.hasLength(path) && path.charAt(0) != PATH_DELIMITER) { - path = PATH_DELIMITER + path; - } - uriVariables.put("basePath", (path != null) ? path : ""); - uriVariables.put("baseUrl", uriComponents.toUriString()); - return uriVariables; - } - - private static String getApplicationUri(HttpServletRequest request) { - UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) - .replacePath(request.getContextPath()) - .replaceQuery(null) - .fragment(null) - .build(); - return uriComponents.toUriString(); - } - - /** - * A class for resolving {@link RelyingPartyRegistration} URIs - */ - public static final class UriResolver { - - private final Map uriVariables; - - private UriResolver(Map uriVariables) { - this.uriVariables = uriVariables; - } - - public String resolve(String uri) { - if (uri == null) { - return null; - } - return UriComponentsBuilder.fromUriString(uri).buildAndExpand(this.uriVariables).toUriString(); - } - - } - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java index b195e4945a..194383a552 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java @@ -27,12 +27,8 @@ import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.security.saml2.Saml2Exception; import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResolver; -import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponse; -import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResponseResolver; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -49,68 +45,52 @@ public final class Saml2MetadataFilter extends OncePerRequestFilter { public static final String DEFAULT_METADATA_FILE_NAME = "saml-{registrationId}-metadata.xml"; - private final Saml2MetadataResponseResolver metadataResolver; + private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver; + + private final Saml2MetadataResolver saml2MetadataResolver; + + private String metadataFilename = DEFAULT_METADATA_FILE_NAME; + + private RequestMatcher requestMatcher = new AntPathRequestMatcher( + "/saml2/service-provider-metadata/{registrationId}"); public Saml2MetadataFilter(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver, Saml2MetadataResolver saml2MetadataResolver) { Assert.notNull(relyingPartyRegistrationResolver, "relyingPartyRegistrationResolver cannot be null"); Assert.notNull(saml2MetadataResolver, "saml2MetadataResolver cannot be null"); - this.metadataResolver = new Saml2MetadataResponseResolverAdapter(relyingPartyRegistrationResolver, - saml2MetadataResolver); - } - - /** - * Constructs an instance of {@link Saml2MetadataFilter} using the provided - * parameters. The {@link #metadataResolver} field will be initialized with a - * {@link DefaultRelyingPartyRegistrationResolver} instance using the provided - * {@link RelyingPartyRegistrationRepository} - * @param relyingPartyRegistrationRepository the - * {@link RelyingPartyRegistrationRepository} to use - * @param saml2MetadataResolver the {@link Saml2MetadataResolver} to use - * @since 6.1 - */ - public Saml2MetadataFilter(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository, - Saml2MetadataResolver saml2MetadataResolver) { - this(new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository), saml2MetadataResolver); - } - - /** - * Constructs an instance of {@link Saml2MetadataFilter} - * @param metadataResponseResolver the strategy for producing metadata - * @since 6.1 - */ - public Saml2MetadataFilter(Saml2MetadataResponseResolver metadataResponseResolver) { - this.metadataResolver = metadataResponseResolver; + this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver; + this.saml2MetadataResolver = saml2MetadataResolver; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { - Saml2MetadataResponse metadata; - try { - metadata = this.metadataResolver.resolve(request); - } - catch (Saml2Exception ex) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } - if (metadata == null) { + RequestMatcher.MatchResult matcher = this.requestMatcher.matcher(request); + if (!matcher.isMatch()) { chain.doFilter(request, response); return; } - writeMetadataToResponse(response, metadata); + String registrationId = matcher.getVariables().get("registrationId"); + RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationResolver.resolve(request, + registrationId); + if (relyingPartyRegistration == null) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + String metadata = this.saml2MetadataResolver.resolve(relyingPartyRegistration); + writeMetadataToResponse(response, relyingPartyRegistration.getRegistrationId(), metadata); } - private void writeMetadataToResponse(HttpServletResponse response, Saml2MetadataResponse metadata) + private void writeMetadataToResponse(HttpServletResponse response, String registrationId, String metadata) throws IOException { response.setContentType(MediaType.APPLICATION_XML_VALUE); - String format = "attachment; filename=\"%s\"; filename*=UTF-8''%s"; - String fileName = metadata.getFileName(); + String fileName = this.metadataFilename.replace("{registrationId}", registrationId); String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()); + String format = "attachment; filename=\"%s\"; filename*=UTF-8''%s"; response.setHeader(HttpHeaders.CONTENT_DISPOSITION, String.format(format, fileName, encodedFileName)); - response.setContentLength(metadata.getMetadata().getBytes(StandardCharsets.UTF_8).length); + response.setContentLength(metadata.getBytes(StandardCharsets.UTF_8).length); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - response.getWriter().write(metadata.getMetadata()); + response.getWriter().write(metadata); } /** @@ -120,9 +100,7 @@ public final class Saml2MetadataFilter extends OncePerRequestFilter { */ public void setRequestMatcher(RequestMatcher requestMatcher) { Assert.notNull(requestMatcher, "requestMatcher cannot be null"); - Assert.isInstanceOf(Saml2MetadataResponseResolverAdapter.class, this.metadataResolver, - "a Saml2MetadataResponseResolver and RequestMatcher cannot be both set on this filter. Please set the request matcher on the Saml2MetadataResponseResolver itself."); - ((Saml2MetadataResponseResolverAdapter) this.metadataResolver).setRequestMatcher(requestMatcher); + this.requestMatcher = requestMatcher; } /** @@ -138,57 +116,7 @@ public final class Saml2MetadataFilter extends OncePerRequestFilter { Assert.hasText(metadataFilename, "metadataFilename cannot be empty"); Assert.isTrue(metadataFilename.contains("{registrationId}"), "metadataFilename must contain a {registrationId} match variable"); - Assert.isInstanceOf(Saml2MetadataResponseResolverAdapter.class, this.metadataResolver, - "a Saml2MetadataResponseResolver and file name cannot be both set on this filter. Please set the file name on the Saml2MetadataResponseResolver itself."); - ((Saml2MetadataResponseResolverAdapter) this.metadataResolver).setMetadataFilename(metadataFilename); - } - - private static final class Saml2MetadataResponseResolverAdapter implements Saml2MetadataResponseResolver { - - private final RelyingPartyRegistrationResolver registrations; - - private RequestMatcher requestMatcher = new AntPathRequestMatcher( - "/saml2/service-provider-metadata/{registrationId}"); - - private final Saml2MetadataResolver metadataResolver; - - private String metadataFilename = DEFAULT_METADATA_FILE_NAME; - - Saml2MetadataResponseResolverAdapter(RelyingPartyRegistrationResolver registrations, - Saml2MetadataResolver metadataResolver) { - this.registrations = registrations; - this.metadataResolver = metadataResolver; - } - - @Override - public Saml2MetadataResponse resolve(HttpServletRequest request) { - RequestMatcher.MatchResult matcher = this.requestMatcher.matcher(request); - if (!matcher.isMatch()) { - return null; - } - String registrationId = matcher.getVariables().get("registrationId"); - RelyingPartyRegistration relyingPartyRegistration = this.registrations.resolve(request, registrationId); - if (relyingPartyRegistration == null) { - throw new Saml2Exception("registration not found"); - } - registrationId = relyingPartyRegistration.getRegistrationId(); - String metadata = this.metadataResolver.resolve(relyingPartyRegistration); - String fileName = this.metadataFilename.replace("{registrationId}", registrationId); - return new Saml2MetadataResponse(metadata, fileName); - } - - void setRequestMatcher(RequestMatcher requestMatcher) { - Assert.notNull(requestMatcher, "requestMatcher cannot be null"); - this.requestMatcher = requestMatcher; - } - - void setMetadataFilename(String metadataFilename) { - Assert.hasText(metadataFilename, "metadataFilename cannot be empty"); - Assert.isTrue(metadataFilename.contains("{registrationId}"), - "metadataFilename must contain a {registrationId} match variable"); - this.metadataFilename = metadataFilename; - } - + this.metadataFilename = metadataFilename; } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolver.java index e0c434e21c..37749b539f 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolver.java @@ -26,7 +26,6 @@ import org.opensaml.saml.saml2.core.AuthnRequest; import org.springframework.core.convert.converter.Converter; import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -47,20 +46,6 @@ public final class OpenSaml4AuthenticationRequestResolver implements Saml2Authen private Clock clock = Clock.systemUTC(); - /** - * Construct an {@link OpenSaml4AuthenticationRequestResolver} - * @param registrations a repository for relying and asserting party configuration - * @since 6.1 - */ - public OpenSaml4AuthenticationRequestResolver(RelyingPartyRegistrationRepository registrations) { - this.authnRequestResolver = new OpenSamlAuthenticationRequestResolver((request, id) -> { - if (id == null) { - return null; - } - return registrations.findByRegistrationId(id); - }); - } - /** * Construct a {@link OpenSaml4AuthenticationRequestResolver} */ diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java index 39cd8bafb2..415dcec9a7 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java @@ -47,8 +47,6 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2P import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver; import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -92,7 +90,7 @@ class OpenSamlAuthenticationRequestResolver { XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); this.marshaller = (AuthnRequestMarshaller) registry.getMarshallerFactory() .getMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME); - Assert.notNull(this.marshaller, "authnRequestMarshaller must be configured in OpenSAML"); + Assert.notNull(this.marshaller, "logoutRequestMarshaller must be configured in OpenSAML"); this.authnRequestBuilder = (AuthnRequestBuilder) XMLObjectProviderRegistrySupport.getBuilderFactory() .getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); Assert.notNull(this.authnRequestBuilder, "authnRequestBuilder must be configured in OpenSAML"); @@ -129,19 +127,15 @@ class OpenSamlAuthenticationRequestResolver { if (registration == null) { return null; } - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); - String entityId = uriResolver.resolve(registration.getEntityId()); - String assertionConsumerServiceLocation = uriResolver - .resolve(registration.getAssertionConsumerServiceLocation()); AuthnRequest authnRequest = this.authnRequestBuilder.buildObject(); authnRequest.setForceAuthn(Boolean.FALSE); authnRequest.setIsPassive(Boolean.FALSE); authnRequest.setProtocolBinding(registration.getAssertionConsumerServiceBinding().getUrn()); Issuer iss = this.issuerBuilder.buildObject(); - iss.setValue(entityId); + iss.setValue(registration.getEntityId()); authnRequest.setIssuer(iss); authnRequest.setDestination(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); - authnRequest.setAssertionConsumerServiceURL(assertionConsumerServiceLocation); + authnRequest.setAssertionConsumerServiceURL(registration.getAssertionConsumerServiceLocation()); if (registration.getNameIdFormat() != null) { NameIDPolicy nameIdPolicy = this.nameIdPolicyBuilder.buildObject(); nameIdPolicy.setFormat(registration.getNameIdFormat()); @@ -154,8 +148,7 @@ class OpenSamlAuthenticationRequestResolver { String relayState = this.relayStateResolver.convert(request); Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleSignOnServiceBinding(); if (binding == Saml2MessageBinding.POST) { - if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned() - || registration.isAuthnRequestsSigned()) { + if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) { OpenSamlSigningUtils.sign(authnRequest, registration); } String xml = serialize(authnRequest); @@ -174,8 +167,7 @@ class OpenSamlAuthenticationRequestResolver { .samlRequest(deflatedAndEncoded) .relayState(relayState) .id(authnRequest.getID()); - if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned() - || registration.isAuthnRequestsSigned()) { + if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) { OpenSamlSigningUtils.QueryParametersPartial parametersPartial = OpenSamlSigningUtils.sign(registration) .param(Saml2ParameterNames.SAML_REQUEST, deflatedAndEncoded); if (relayState != null) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilter.java index 334e8a51a8..4affc10049 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilter.java @@ -35,9 +35,6 @@ import org.springframework.security.saml2.provider.service.web.Saml2Authenticati import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.OrRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; /** @@ -47,9 +44,6 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/saml2/sso/{registrationId}"; - private static final RequestMatcher DEFAULT_REQUEST_MATCHER = new OrRequestMatcher( - new AntPathRequestMatcher(DEFAULT_FILTER_PROCESSES_URI), new AntPathRequestMatcher("/login/saml2/sso")); - private final AuthenticationConverter authenticationConverter; private Saml2AuthenticationRequestRepository authenticationRequestRepository = new HttpSessionSaml2AuthenticationRequestRepository(); @@ -81,21 +75,6 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce "filterProcessesUrl must contain a {registrationId} match variable"); } - /** - * Creates a {@link Saml2WebSsoAuthenticationFilter} that is configured to use the - * {@link #DEFAULT_FILTER_PROCESSES_URI} processing URL - * @param authenticationConverter the strategy for converting an - * {@link HttpServletRequest} into an {@link Authentication} - * @since 6.2 - */ - public Saml2WebSsoAuthenticationFilter(AuthenticationConverter authenticationConverter) { - super(DEFAULT_REQUEST_MATCHER); - Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); - this.authenticationConverter = authenticationConverter; - setAllowSessionCreation(true); - setSessionAuthenticationStrategy(new ChangeSessionIdAuthenticationStrategy()); - } - /** * Creates a {@link Saml2WebSsoAuthenticationFilter} given the provided parameters * @param authenticationConverter the strategy for converting an @@ -150,7 +129,8 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce private void setAuthenticationRequestRepositoryIntoAuthenticationConverter( Saml2AuthenticationRequestRepository authenticationRequestRepository) { - if (this.authenticationConverter instanceof Saml2AuthenticationTokenConverter authenticationTokenConverter) { + if (this.authenticationConverter instanceof Saml2AuthenticationTokenConverter) { + Saml2AuthenticationTokenConverter authenticationTokenConverter = (Saml2AuthenticationTokenConverter) this.authenticationConverter; authenticationTokenConverter.setAuthenticationRequestRepository(authenticationRequestRepository); } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolver.java index 8180791f04..c0a652bd41 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,9 @@ import java.util.function.Consumer; import jakarta.servlet.http.HttpServletRequest; import org.opensaml.saml.saml2.core.LogoutRequest; -import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.Authentication; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.util.Assert; @@ -36,7 +34,6 @@ import org.springframework.util.Assert; * OpenSAML 4 * * @author Josh Cummings - * @author Gerhard Haege * @since 5.6 */ public final class OpenSaml4LogoutRequestResolver implements Saml2LogoutRequestResolver { @@ -48,15 +45,6 @@ public final class OpenSaml4LogoutRequestResolver implements Saml2LogoutRequestR private Clock clock = Clock.systemUTC(); - public OpenSaml4LogoutRequestResolver(RelyingPartyRegistrationRepository registrations) { - this((request, id) -> { - if (id == null) { - return null; - } - return registrations.findByRegistrationId(id); - }); - } - /** * Construct a {@link OpenSaml4LogoutRequestResolver} */ @@ -95,16 +83,6 @@ public final class OpenSaml4LogoutRequestResolver implements Saml2LogoutRequestR this.clock = clock; } - /** - * Use this {@link Converter} to compute the RelayState - * @param relayStateResolver the {@link Converter} to use - * @since 6.1 - */ - public void setRelayStateResolver(Converter relayStateResolver) { - Assert.notNull(relayStateResolver, "relayStateResolver cannot be null"); - this.logoutRequestResolver.setRelayStateResolver(relayStateResolver); - } - public static final class LogoutRequestParameters { private final HttpServletRequest request; diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolver.java index 6e95b3dae1..8076c76e99 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolver.java @@ -26,7 +26,6 @@ import org.opensaml.saml.saml2.core.LogoutResponse; import org.springframework.security.core.Authentication; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.util.Assert; @@ -46,20 +45,11 @@ public final class OpenSaml4LogoutResponseResolver implements Saml2LogoutRespons private Clock clock = Clock.systemUTC(); - public OpenSaml4LogoutResponseResolver(RelyingPartyRegistrationRepository registrations) { - this.logoutResponseResolver = new OpenSamlLogoutResponseResolver(registrations, (request, id) -> { - if (id == null) { - return null; - } - return registrations.findByRegistrationId(id); - }); - } - /** * Construct a {@link OpenSaml4LogoutResponseResolver} */ public OpenSaml4LogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { - this.logoutResponseResolver = new OpenSamlLogoutResponseResolver(null, relyingPartyRegistrationResolver); + this.logoutResponseResolver = new OpenSamlLogoutResponseResolver(relyingPartyRegistrationResolver); } /** diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java index 69d9ccc4a1..03b719657b 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,6 @@ import org.opensaml.saml.saml2.core.impl.NameIDBuilder; import org.opensaml.saml.saml2.core.impl.SessionIndexBuilder; import org.w3c.dom.Element; -import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.Authentication; import org.springframework.security.saml2.Saml2Exception; import org.springframework.security.saml2.core.OpenSamlInitializationService; @@ -47,8 +46,6 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver; import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial; import org.springframework.util.Assert; @@ -77,8 +74,6 @@ final class OpenSamlLogoutRequestResolver { private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver; - private Converter relayStateResolver = (request) -> UUID.randomUUID().toString(); - /** * Construct a {@link OpenSamlLogoutRequestResolver} */ @@ -100,10 +95,6 @@ final class OpenSamlLogoutRequestResolver { Assert.notNull(this.sessionIndexBuilder, "sessionIndexBuilder must be configured in OpenSAML"); } - void setRelayStateResolver(Converter relayStateResolver) { - this.relayStateResolver = relayStateResolver; - } - /** * Prepare to create, sign, and serialize a SAML 2.0 Logout Request. * @@ -129,12 +120,10 @@ final class OpenSamlLogoutRequestResolver { if (registration.getAssertingPartyDetails().getSingleLogoutServiceLocation() == null) { return null; } - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); - String entityId = uriResolver.resolve(registration.getEntityId()); LogoutRequest logoutRequest = this.logoutRequestBuilder.buildObject(); logoutRequest.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation()); Issuer issuer = this.issuerBuilder.buildObject(); - issuer.setValue(entityId); + issuer.setValue(registration.getEntityId()); logoutRequest.setIssuer(issuer); NameID nameId = this.nameIdBuilder.buildObject(); nameId.setValue(authentication.getName()); @@ -143,7 +132,7 @@ final class OpenSamlLogoutRequestResolver { Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); for (String index : principal.getSessionIndexes()) { SessionIndex sessionIndex = this.sessionIndexBuilder.buildObject(); - sessionIndex.setValue(index); + sessionIndex.setSessionIndex(index); logoutRequest.getSessionIndexes().add(sessionIndex); } } @@ -151,7 +140,7 @@ final class OpenSamlLogoutRequestResolver { if (logoutRequest.getID() == null) { logoutRequest.setID("LR" + UUID.randomUUID()); } - String relayState = this.relayStateResolver.convert(request); + String relayState = UUID.randomUUID().toString(); Saml2LogoutRequest.Builder result = Saml2LogoutRequest.withRelyingPartyRegistration(registration) .id(logoutRequest.getID()); if (registration.getAssertingPartyDetails().getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolver.java deleted file mode 100644 index 7e005c79d2..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolver.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright 2002-2023 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.saml2.provider.service.web.authentication.logout; - -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; - -import jakarta.servlet.http.HttpServletRequest; -import net.shibboleth.utilities.java.support.xml.ParserPool; -import org.opensaml.core.config.ConfigurationService; -import org.opensaml.core.xml.config.XMLObjectProviderRegistry; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; -import org.opensaml.saml.saml2.core.LogoutRequest; -import org.opensaml.saml.saml2.core.impl.LogoutRequestUnmarshaller; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import org.springframework.http.HttpMethod; -import org.springframework.security.core.Authentication; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.core.OpenSamlInitializationService; -import org.springframework.security.saml2.core.Saml2Error; -import org.springframework.security.saml2.core.Saml2ErrorCodes; -import org.springframework.security.saml2.core.Saml2ParameterNames; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; -import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutRequestValidator; -import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; -import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidatorParameters; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.OrRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.Assert; - -/** - * An OpenSAML-based implementation of - * {@link Saml2LogoutRequestValidatorParametersResolver} - */ -public final class OpenSamlLogoutRequestValidatorParametersResolver - implements Saml2LogoutRequestValidatorParametersResolver { - - static { - OpenSamlInitializationService.initialize(); - } - - private RequestMatcher requestMatcher = new OrRequestMatcher( - new AntPathRequestMatcher("/logout/saml2/slo/{registrationId}"), - new AntPathRequestMatcher("/logout/saml2/slo")); - - private final RelyingPartyRegistrationRepository registrations; - - private final ParserPool parserPool; - - private final LogoutRequestUnmarshaller unmarshaller; - - /** - * Constructs a {@link OpenSamlLogoutRequestValidator} - */ - public OpenSamlLogoutRequestValidatorParametersResolver(RelyingPartyRegistrationRepository registrations) { - Assert.notNull(registrations, "relyingPartyRegistrationRepository cannot be null"); - XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); - this.parserPool = registry.getParserPool(); - this.unmarshaller = (LogoutRequestUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory() - .getUnmarshaller(LogoutRequest.DEFAULT_ELEMENT_NAME); - this.registrations = registrations; - } - - /** - * Construct the parameters necessary for validating an asserting party's - * {@code } based on the given {@link HttpServletRequest} - * - *

      - * Uses the configured {@link RequestMatcher} to identify the processing request, - * including looking for any indicated {@code registrationId}. - * - *

      - * If a {@code registrationId} is found in the request, it will attempt to use that, - * erroring if no {@link RelyingPartyRegistration} is found. - * - *

      - * If no {@code registrationId} is found in the request, it will look for a currently - * logged-in user and use the associated {@code registrationId}. - * - *

      - * In the event that neither the URL nor any logged in user could determine a - * {@code registrationId}, this code then will try and derive a - * {@link RelyingPartyRegistration} given the {@code }'s - * {@code Issuer} value. - * @param request the HTTP request - * @return a {@link Saml2LogoutRequestValidatorParameters} instance, or {@code null} - * if one could not be resolved - * @throws Saml2AuthenticationException if the {@link RequestMatcher} specifies a - * non-existent {@code registrationId} - */ - @Override - public Saml2LogoutRequestValidatorParameters resolve(HttpServletRequest request, Authentication authentication) { - if (request.getParameter(Saml2ParameterNames.SAML_REQUEST) == null) { - return null; - } - RequestMatcher.MatchResult result = this.requestMatcher.matcher(request); - if (!result.isMatch()) { - return null; - } - String registrationId = getRegistrationId(result, authentication); - if (registrationId == null) { - return logoutRequestByEntityId(request, authentication); - } - return logoutRequestById(request, authentication, registrationId); - } - - /** - * The request matcher to use to identify a request to process a - * {@code }. By default, checks for {@code /logout/saml2/slo} and - * {@code /logout/saml2/slo/{registrationId}}. - * - *

      - * Generally speaking, the URL does not need to have a {@code registrationId} in it - * since either it can be looked up from the active logged in user or it can be - * derived through the {@code Issuer} in the {@code }. - * @param requestMatcher the {@link RequestMatcher} to use - */ - public void setRequestMatcher(RequestMatcher requestMatcher) { - Assert.notNull(requestMatcher, "requestMatcher cannot be null"); - this.requestMatcher = requestMatcher; - } - - private String getRegistrationId(RequestMatcher.MatchResult result, Authentication authentication) { - String registrationId = result.getVariables().get("registrationId"); - if (registrationId != null) { - return registrationId; - } - if (authentication == null) { - return null; - } - if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal principal) { - return principal.getRelyingPartyRegistrationId(); - } - return null; - } - - private Saml2LogoutRequestValidatorParameters logoutRequestById(HttpServletRequest request, - Authentication authentication, String registrationId) { - RelyingPartyRegistration registration = this.registrations.findByRegistrationId(registrationId); - if (registration == null) { - throw new Saml2AuthenticationException( - new Saml2Error(Saml2ErrorCodes.RELYING_PARTY_REGISTRATION_NOT_FOUND, "registration not found"), - "registration not found"); - } - return logoutRequestByRegistration(request, registration, authentication); - } - - private Saml2LogoutRequestValidatorParameters logoutRequestByEntityId(HttpServletRequest request, - Authentication authentication) { - String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST); - byte[] b = Saml2Utils.samlDecode(serialized); - LogoutRequest logoutRequest = parse(inflateIfRequired(request, b)); - String issuer = logoutRequest.getIssuer().getValue(); - RelyingPartyRegistration registration = this.registrations.findUniqueByAssertingPartyEntityId(issuer); - return logoutRequestByRegistration(request, registration, authentication); - } - - private Saml2LogoutRequestValidatorParameters logoutRequestByRegistration(HttpServletRequest request, - RelyingPartyRegistration registration, Authentication authentication) { - if (registration == null) { - return null; - } - Saml2MessageBinding saml2MessageBinding = Saml2MessageBindingUtils.resolveBinding(request); - registration = fromRequest(request, registration); - String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST); - Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) - .samlRequest(serialized) - .relayState(request.getParameter(Saml2ParameterNames.RELAY_STATE)) - .binding(saml2MessageBinding) - .location(registration.getSingleLogoutServiceLocation()) - .parameters((params) -> params.put(Saml2ParameterNames.SIG_ALG, - request.getParameter(Saml2ParameterNames.SIG_ALG))) - .parameters((params) -> params.put(Saml2ParameterNames.SIGNATURE, - request.getParameter(Saml2ParameterNames.SIGNATURE))) - .parametersQuery((params) -> request.getQueryString()) - .build(); - return new Saml2LogoutRequestValidatorParameters(logoutRequest, registration, authentication); - } - - private String inflateIfRequired(HttpServletRequest request, byte[] b) { - if (HttpMethod.GET.matches(request.getMethod())) { - return Saml2Utils.samlInflate(b); - } - return new String(b, StandardCharsets.UTF_8); - } - - private LogoutRequest parse(String request) throws Saml2Exception { - try { - Document document = this.parserPool - .parse(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8))); - Element element = document.getDocumentElement(); - return (LogoutRequest) this.unmarshaller.unmarshall(element); - } - catch (Exception ex) { - throw new Saml2Exception("Failed to deserialize LogoutRequest", ex); - } - } - - private RelyingPartyRegistration fromRequest(HttpServletRequest request, RelyingPartyRegistration registration) { - RelyingPartyRegistrationPlaceholderResolvers.UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers - .uriResolver(request, registration); - String entityId = uriResolver.resolve(registration.getEntityId()); - String logoutLocation = uriResolver.resolve(registration.getSingleLogoutServiceLocation()); - String logoutResponseLocation = uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation()); - return registration.mutate() - .entityId(entityId) - .singleLogoutServiceLocation(logoutLocation) - .singleLogoutServiceResponseLocation(logoutResponseLocation) - .build(); - } - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java index b726eb6d32..04aaa7a316 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java @@ -51,10 +51,7 @@ import org.springframework.security.saml2.core.Saml2ParameterNames; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver; import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlSigningUtils.QueryParametersPartial; import org.springframework.util.Assert; @@ -85,16 +82,12 @@ final class OpenSamlLogoutResponseResolver { private final StatusCodeBuilder statusCodeBuilder; - private final RelyingPartyRegistrationRepository registrations; - private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver; /** * Construct a {@link OpenSamlLogoutResponseResolver} */ - OpenSamlLogoutResponseResolver(RelyingPartyRegistrationRepository registrations, - RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { - this.registrations = registrations; + OpenSamlLogoutResponseResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver; XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class); this.parserPool = registry.getParserPool(); @@ -133,25 +126,19 @@ final class OpenSamlLogoutResponseResolver { Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentication, BiConsumer logoutResponseConsumer) { - LogoutRequest logoutRequest = parse(extractSamlRequest(request)); String registrationId = getRegistrationId(authentication); RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request, registrationId); - if (registration == null && this.registrations != null) { - String issuer = logoutRequest.getIssuer().getValue(); - registration = this.registrations.findUniqueByAssertingPartyEntityId(issuer); - } if (registration == null) { return null; } if (registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation() == null) { return null; } - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); - String entityId = uriResolver.resolve(registration.getEntityId()); + LogoutRequest logoutRequest = parse(extractSamlRequest(request)); LogoutResponse logoutResponse = this.logoutResponseBuilder.buildObject(); logoutResponse.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation()); Issuer issuer = this.issuerBuilder.buildObject(); - issuer.setValue(entityId); + issuer.setValue(registration.getEntityId()); logoutResponse.setIssuer(issuer); StatusCode code = this.statusCodeBuilder.buildObject(); code.setValue(StatusCode.SUCCESS); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java index 61a71ed31a..15b481a3ee 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java @@ -30,11 +30,8 @@ import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; -import org.springframework.security.saml2.core.Saml2Error; -import org.springframework.security.saml2.core.Saml2ErrorCodes; import org.springframework.security.saml2.core.Saml2ParameterNames; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidatorParameters; @@ -42,8 +39,6 @@ import org.springframework.security.saml2.provider.service.authentication.logout import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver; import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; @@ -72,24 +67,17 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter { private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); - private final Saml2LogoutRequestValidatorParametersResolver logoutRequestResolver; - private final Saml2LogoutRequestValidator logoutRequestValidator; + private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver; + private final Saml2LogoutResponseResolver logoutResponseResolver; private final LogoutHandler handler; private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); - public Saml2LogoutRequestFilter(Saml2LogoutRequestValidatorParametersResolver logoutRequestResolver, - Saml2LogoutRequestValidator logoutRequestValidator, Saml2LogoutResponseResolver logoutResponseResolver, - LogoutHandler... handlers) { - this.logoutRequestResolver = logoutRequestResolver; - this.logoutRequestValidator = logoutRequestValidator; - this.logoutResponseResolver = logoutResponseResolver; - this.handler = new CompositeLogoutHandler(handlers); - } + private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher("/logout/saml2/slo"); /** * Constructs a {@link Saml2LogoutResponseFilter} for accepting SAML 2.0 Logout @@ -103,7 +91,7 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter { public Saml2LogoutRequestFilter(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver, Saml2LogoutRequestValidator logoutRequestValidator, Saml2LogoutResponseResolver logoutResponseResolver, LogoutHandler... handlers) { - this.logoutRequestResolver = new Saml2AssertingPartyLogoutRequestResolver(relyingPartyRegistrationResolver); + this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver; this.logoutRequestValidator = logoutRequestValidator; this.logoutResponseResolver = logoutResponseResolver; this.handler = new CompositeLogoutHandler(handlers); @@ -112,21 +100,26 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { - Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication(); - Saml2LogoutRequestValidatorParameters parameters; - try { - parameters = this.logoutRequestResolver.resolve(request, authentication); - } - catch (Saml2AuthenticationException ex) { - this.logger.trace("Did not process logout request since failed to find requested RelyingPartyRegistration"); - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - if (parameters == null) { + + if (!this.logoutRequestMatcher.matches(request)) { chain.doFilter(request, response); return; } - RelyingPartyRegistration registration = parameters.getRelyingPartyRegistration(); + + if (request.getParameter(Saml2ParameterNames.SAML_REQUEST) == null) { + chain.doFilter(request, response); + return; + } + + Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication(); + RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request, + getRegistrationId(authentication)); + if (registration == null) { + this.logger + .trace("Did not process logout request since failed to find associated RelyingPartyRegistration"); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } if (registration.getSingleLogoutServiceLocation() == null) { this.logger.trace( "Did not process logout request since RelyingPartyRegistration has not been configured with a logout request endpoint"); @@ -141,6 +134,20 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter { return; } + String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest(serialized) + .relayState(request.getParameter(Saml2ParameterNames.RELAY_STATE)) + .binding(saml2MessageBinding) + .location(registration.getSingleLogoutServiceLocation()) + .parameters((params) -> params.put(Saml2ParameterNames.SIG_ALG, + request.getParameter(Saml2ParameterNames.SIG_ALG))) + .parameters((params) -> params.put(Saml2ParameterNames.SIGNATURE, + request.getParameter(Saml2ParameterNames.SIGNATURE))) + .parametersQuery((params) -> request.getQueryString()) + .build(); + Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(logoutRequest, + registration, authentication); Saml2LogoutValidatorResult result = this.logoutRequestValidator.validate(parameters); if (result.hasErrors()) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, result.getErrors().iterator().next().toString()); @@ -164,10 +171,7 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter { public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) { Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null"); - Assert.isInstanceOf(Saml2AssertingPartyLogoutRequestResolver.class, this.logoutRequestResolver, - "saml2LogoutRequestResolver and logoutRequestMatcher cannot both be set. Please set the request matcher in the saml2LogoutRequestResolver itself."); - ((Saml2AssertingPartyLogoutRequestResolver) this.logoutRequestResolver) - .setLogoutRequestMatcher(logoutRequestMatcher); + this.logoutRequestMatcher = logoutRequestMatcher; } /** @@ -181,6 +185,17 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter { this.securityContextHolderStrategy = securityContextHolderStrategy; } + private String getRegistrationId(Authentication authentication) { + if (authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if (principal instanceof Saml2AuthenticatedPrincipal) { + return ((Saml2AuthenticatedPrincipal) principal).getRelyingPartyRegistrationId(); + } + return null; + } + private void doRedirect(HttpServletRequest request, HttpServletResponse response, Saml2LogoutResponse logoutResponse) throws IOException { String location = logoutResponse.getResponseLocation(); @@ -240,79 +255,4 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter { return html.toString(); } - private static class Saml2AssertingPartyLogoutRequestResolver - implements Saml2LogoutRequestValidatorParametersResolver { - - private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver; - - private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher("/logout/saml2/slo"); - - Saml2AssertingPartyLogoutRequestResolver(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { - this.relyingPartyRegistrationResolver = relyingPartyRegistrationResolver; - } - - @Override - public Saml2LogoutRequestValidatorParameters resolve(HttpServletRequest request, - Authentication authentication) { - String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST); - if (serialized == null) { - return null; - } - RequestMatcher.MatchResult result = this.logoutRequestMatcher.matcher(request); - if (!result.isMatch()) { - return null; - } - String registrationId = getRegistrationId(result, authentication); - RelyingPartyRegistration registration = this.relyingPartyRegistrationResolver.resolve(request, - registrationId); - if (registration == null) { - throw new Saml2AuthenticationException( - new Saml2Error(Saml2ErrorCodes.RELYING_PARTY_REGISTRATION_NOT_FOUND, "registration not found"), - "registration not found"); - } - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); - String entityId = uriResolver.resolve(registration.getEntityId()); - String logoutLocation = uriResolver.resolve(registration.getSingleLogoutServiceLocation()); - String logoutResponseLocation = uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation()); - registration = registration.mutate() - .entityId(entityId) - .singleLogoutServiceLocation(logoutLocation) - .singleLogoutServiceResponseLocation(logoutResponseLocation) - .build(); - Saml2MessageBinding saml2MessageBinding = Saml2MessageBindingUtils.resolveBinding(request); - Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) - .samlRequest(serialized) - .relayState(request.getParameter(Saml2ParameterNames.RELAY_STATE)) - .binding(saml2MessageBinding) - .location(registration.getSingleLogoutServiceLocation()) - .parameters((params) -> params.put(Saml2ParameterNames.SIG_ALG, - request.getParameter(Saml2ParameterNames.SIG_ALG))) - .parameters((params) -> params.put(Saml2ParameterNames.SIGNATURE, - request.getParameter(Saml2ParameterNames.SIGNATURE))) - .parametersQuery((params) -> request.getQueryString()) - .build(); - return new Saml2LogoutRequestValidatorParameters(logoutRequest, registration, authentication); - } - - void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) { - Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null"); - this.logoutRequestMatcher = logoutRequestMatcher; - } - - private String getRegistrationId(RequestMatcher.MatchResult result, Authentication authentication) { - String registrationId = result.getVariables().get("registrationId"); - if (registrationId != null) { - return registrationId; - } - if (authentication == null) { - return null; - } - if (authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal principal) { - return principal.getRelyingPartyRegistrationId(); - } - return null; - } - - } - } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestValidatorParametersResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestValidatorParametersResolver.java deleted file mode 100644 index 46adcea02a..0000000000 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestValidatorParametersResolver.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2002-2023 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.saml2.provider.service.web.authentication.logout; - -import jakarta.servlet.http.HttpServletRequest; - -import org.springframework.security.core.Authentication; -import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidatorParameters; - -/** - * Resolved a SAML 2.0 Logout Request and associated validation parameters from the given - * {@link HttpServletRequest} and current {@link Authentication}. - * - * The returned logout request is suitable for validating, logging out the logged-in user, - * and initiating the construction of a {@code LogoutResponse}. - * - * @author Josh Cummings - * @since 6.1 - */ -public interface Saml2LogoutRequestValidatorParametersResolver { - - /** - * Resolve any SAML 2.0 Logout Request and associated - * {@link org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration} - * @param request the HTTP request - * @param authentication the current user, if any; may be null - * @return a SAML 2.0 Logout Request, if any; may be null - */ - Saml2LogoutRequestValidatorParameters resolve(HttpServletRequest request, Authentication authentication); - -} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java index a3891bc541..ea698663b7 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java @@ -35,10 +35,7 @@ import org.springframework.security.saml2.provider.service.authentication.logout import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidatorParameters; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver; import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -74,18 +71,6 @@ public final class Saml2LogoutResponseFilter extends OncePerRequestFilter { private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher("/logout/saml2/slo"); - public Saml2LogoutResponseFilter(RelyingPartyRegistrationRepository registrations, - Saml2LogoutResponseValidator logoutResponseValidator, LogoutSuccessHandler logoutSuccessHandler) { - this.relyingPartyRegistrationResolver = (request, id) -> { - if (id == null) { - return null; - } - return registrations.findByRegistrationId(id); - }; - this.logoutResponseValidator = logoutResponseValidator; - this.logoutSuccessHandler = logoutSuccessHandler; - } - /** * Constructs a {@link Saml2LogoutResponseFilter} for accepting SAML 2.0 Logout * Responses from the asserting party @@ -140,15 +125,7 @@ public final class Saml2LogoutResponseFilter extends OncePerRequestFilter { response.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); - String entityId = uriResolver.resolve(registration.getEntityId()); - String logoutLocation = uriResolver.resolve(registration.getSingleLogoutServiceLocation()); - String logoutResponseLocation = uriResolver.resolve(registration.getSingleLogoutServiceResponseLocation()); - registration = registration.mutate() - .entityId(entityId) - .singleLogoutServiceLocation(logoutLocation) - .singleLogoutServiceResponseLocation(logoutResponseLocation) - .build(); + Saml2MessageBinding saml2MessageBinding = Saml2MessageBindingUtils.resolveBinding(request); if (!registration.getSingleLogoutServiceBindings().contains(saml2MessageBinding)) { this.logger.trace("Did not process logout response since used incorrect binding"); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java index eccc42693f..d14910c36e 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.security.saml2.provider.service.authentication; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.util.Arrays; @@ -47,6 +48,7 @@ import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.AttributeValue; +import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.Conditions; import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.EncryptedAttribute; @@ -76,6 +78,7 @@ import org.springframework.security.saml2.core.TestSaml2X509Credentials; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken; import org.springframework.security.saml2.provider.service.authentication.TestCustomOpenSamlObjects.CustomOpenSamlObject; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; import org.springframework.util.StringUtils; @@ -229,7 +232,8 @@ public class OpenSaml4AuthenticationProviderTests { response.setInResponseTo("SAML2"); response.getAssertions().add(signed(assertion("SAML2"))); response.getAssertions().add(signed(assertion("SAML2"))); - AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2"); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, false); Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); this.provider.authenticate(token); } @@ -239,18 +243,33 @@ public class OpenSaml4AuthenticationProviderTests { Response response = response(); response.getAssertions().add(signed(assertion())); response.getAssertions().add(signed(assertion("SAML2"))); - AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2"); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, false); Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); this.provider.authenticate(token); } + @Test + public void evaluateInResponseToFailsWhenInResponseToInAssertionOnlyAndCorruptedStoredRequest() { + Response response = response(); + response.getAssertions().add(signed(assertion())); + response.getAssertions().add(signed(assertion("SAML2"))); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, true); + Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)) + .withStackTraceContaining("malformed_request_data"); + } + @Test public void evaluateInResponseToFailsWhenInResponseToInAssertionMismatchWithRequestID() { Response response = response(); response.setInResponseTo("SAML2"); response.getAssertions().add(signed(assertion("SAML2"))); response.getAssertions().add(signed(assertion("BAD"))); - AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2"); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, false); Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> this.provider.authenticate(token)) @@ -262,7 +281,8 @@ public class OpenSaml4AuthenticationProviderTests { Response response = response(); response.getAssertions().add(signed(assertion())); response.getAssertions().add(signed(assertion("BAD"))); - AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2"); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, false); Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> this.provider.authenticate(token)) @@ -275,13 +295,28 @@ public class OpenSaml4AuthenticationProviderTests { response.setInResponseTo("BAD"); response.getAssertions().add(signed(assertion("SAML2"))); response.getAssertions().add(signed(assertion("SAML2"))); - AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2"); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, false); Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); assertThatExceptionOfType(Saml2AuthenticationException.class) .isThrownBy(() -> this.provider.authenticate(token)) .withStackTraceContaining("invalid_in_response_to"); } + @Test + public void evaluateInResponseToFailsWhenInResponseInToResponseAndCorruptedStoredRequest() { + Response response = response(); + response.setInResponseTo("SAML2"); + response.getAssertions().add(signed(assertion())); + response.getAssertions().add(signed(assertion())); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, true); + Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); + assertThatExceptionOfType(Saml2AuthenticationException.class) + .isThrownBy(() -> this.provider.authenticate(token)) + .withStackTraceContaining("malformed_request_data"); + } + @Test public void evaluateInResponseToFailsWhenInResponseToInResponseButNoSavedRequest() { Response response = response(); @@ -296,7 +331,8 @@ public class OpenSaml4AuthenticationProviderTests { public void evaluateInResponseToSucceedsWhenNoInResponseToInResponseOrAssertions() { Response response = response(); response.getAssertions().add(signed(assertion())); - AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2"); + AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mockedStoredAuthenticationRequest("SAML2", + Saml2MessageBinding.POST, false); Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest); this.provider.authenticate(token); } @@ -781,6 +817,17 @@ public class OpenSaml4AuthenticationProviderTests { return response; } + private AuthnRequest request() { + AuthnRequest request = TestOpenSamlObjects.authnRequest(); + return request; + } + + private String serializedRequest(AuthnRequest request, Saml2MessageBinding binding) { + String xml = serialize(request); + return (binding == Saml2MessageBinding.POST) ? Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8)) + : Saml2Utils.samlEncode(Saml2Utils.samlDeflate(xml)); + } + private Assertion assertion(String inResponseTo) { Assertion assertion = TestOpenSamlObjects.assertion(); assertion.setIssueInstant(Instant.now()); @@ -836,9 +883,19 @@ public class OpenSaml4AuthenticationProviderTests { return new Saml2AuthenticationToken(registration.build(), serialize(response), authenticationRequest); } - private AbstractSaml2AuthenticationRequest mockedStoredAuthenticationRequest(String requestId) { + private AbstractSaml2AuthenticationRequest mockedStoredAuthenticationRequest(String requestId, + Saml2MessageBinding binding, boolean corruptRequestString) { + AuthnRequest request = request(); + if (requestId != null) { + request.setID(requestId); + } + String serializedRequest = serializedRequest(request, binding); + if (corruptRequestString) { + serializedRequest = serializedRequest.substring(2, serializedRequest.length() - 2); + } AbstractSaml2AuthenticationRequest mockAuthenticationRequest = mock(AbstractSaml2AuthenticationRequest.class); - given(mockAuthenticationRequest.getId()).willReturn(requestId); + given(mockAuthenticationRequest.getSamlRequest()).willReturn(serializedRequest); + given(mockAuthenticationRequest.getBinding()).willReturn(binding); return mockAuthenticationRequest; } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java index 0c5657e200..da8f87723b 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java @@ -17,7 +17,6 @@ package org.springframework.security.saml2.provider.service.authentication; import java.security.cert.X509Certificate; -import java.time.Instant; import java.util.ArrayList; import java.util.Base64; import java.util.List; @@ -102,11 +101,9 @@ public final class TestOpenSamlObjects { private static String DESTINATION = "https://localhost/login/saml2/sso/idp-alias"; - private static String LOGOUT_DESTINATION = "http://localhost/logout/saml2/slo"; - public static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias"; - public static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp"; + private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp"; private static SecretKey SECRET_KEY = new SecretKeySpec( Base64.getDecoder().decode("shOnwNMoCv88HKMEa91+FlYoD5RNvzMTAL5LGxZKIFk="), "AES"); @@ -114,11 +111,11 @@ public final class TestOpenSamlObjects { private TestOpenSamlObjects() { } - public static Response response() { + static Response response() { return response(DESTINATION, ASSERTING_PARTY_ENTITY_ID); } - public static Response response(String destination, String issuerEntityId) { + static Response response(String destination, String issuerEntityId) { Response response = build(Response.DEFAULT_ELEMENT_NAME); response.setID("R" + UUID.randomUUID().toString()); response.setVersion(SAMLVersion.VERSION_20); @@ -144,15 +141,13 @@ public final class TestOpenSamlObjects { return assertion(USERNAME, ASSERTING_PARTY_ENTITY_ID, RELYING_PARTY_ENTITY_ID, DESTINATION); } - public static Assertion assertion(String username, String issuerEntityId, String recipientEntityId, - String recipientUri) { + static Assertion assertion(String username, String issuerEntityId, String recipientEntityId, String recipientUri) { Assertion assertion = build(Assertion.DEFAULT_ELEMENT_NAME); assertion.setID("A" + UUID.randomUUID().toString()); assertion.setVersion(SAMLVersion.VERSION_20); assertion.setIssuer(issuer(issuerEntityId)); assertion.setSubject(subject(username)); assertion.setConditions(conditions()); - assertion.setIssueInstant(Instant.now()); SubjectConfirmation subjectConfirmation = subjectConfirmation(); subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER); SubjectConfirmationData confirmationData = subjectConfirmationData(recipientEntityId); @@ -209,18 +204,6 @@ public final class TestOpenSamlObjects { return authnRequest; } - public static LogoutRequest logoutRequest() { - Issuer issuer = build(Issuer.DEFAULT_ELEMENT_NAME); - issuer.setValue(ASSERTING_PARTY_ENTITY_ID); - NameID nameId = build(NameID.DEFAULT_ELEMENT_NAME); - nameId.setValue("user"); - LogoutRequest logoutRequest = build(LogoutRequest.DEFAULT_ELEMENT_NAME); - logoutRequest.setIssuer(issuer); - logoutRequest.setDestination(LOGOUT_DESTINATION); - logoutRequest.setNameID(nameId); - return logoutRequest; - } - static Credential getSigningCredential(Saml2X509Credential credential, String entityId) { BasicCredential cred = getBasicCredential(credential); cred.setEntityId(entityId); @@ -367,7 +350,7 @@ public final class TestOpenSamlObjects { Attribute websiteAttr = attributeBuilder.buildObject(); websiteAttr.setName("website"); XSURI uri = new XSURIBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSURI.TYPE_NAME); - uri.setURI("https://johndoe.com/"); + uri.setValue("https://johndoe.com/"); websiteAttr.getAttributeValues().add(uri); attrStmt2.getAttributes().add(websiteAttr); Attribute registeredAttr = attributeBuilder.buildObject(); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2Authentications.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2Authentications.java deleted file mode 100644 index 8cfbfc0a29..0000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2Authentications.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2002-2023 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.saml2.provider.service.authentication; - -import java.util.Collections; - -import org.springframework.security.core.authority.AuthorityUtils; - -/** - * Tests instances for {@link Saml2Authentication} - * - * @author Josh Cummings - */ -public final class TestSaml2Authentications { - - private TestSaml2Authentications() { - } - - public static Saml2Authentication authentication() { - DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", - Collections.emptyMap()); - principal.setRelyingPartyRegistrationId("simplesamlphp"); - return new Saml2Authentication(principal, "response", AuthorityUtils.createAuthorityList("ROLE_USER")); - } - -} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java index 189d316b0d..2da33d1c9a 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java @@ -16,8 +16,6 @@ package org.springframework.security.saml2.provider.service.metadata; -import java.util.List; - import org.junit.jupiter.api.Test; import org.springframework.security.saml2.core.TestSaml2X509Credentials; @@ -99,27 +97,4 @@ public class OpenSamlMetadataResolverTests { assertThat(metadata).contains("") - .contains("") - .contains("MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh") - .contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"") - .contains("Location=\"https://rp.example.org/acs\" index=\"1\"") - .contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\""); - } - } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/RequestMatcherMetadataResponseResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/RequestMatcherMetadataResponseResolverTests.java deleted file mode 100644 index 0684218ffd..0000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/RequestMatcherMetadataResponseResolverTests.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2002-2023 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.saml2.provider.service.metadata; - -import java.util.Collection; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -@ExtendWith(MockitoExtension.class) -public final class RequestMatcherMetadataResponseResolverTests { - - @Mock - Saml2MetadataResolver metadataFactory; - - @Test - void saml2MetadataRegistrationIdResolveWhenMatchesThenResolves() { - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); - RelyingPartyRegistrationRepository registrations = new InMemoryRelyingPartyRegistrationRepository(registration); - RequestMatcherMetadataResponseResolver resolver = new RequestMatcherMetadataResponseResolver(registrations, - this.metadataFactory); - String registrationId = registration.getRegistrationId(); - given(this.metadataFactory.resolve(any(Collection.class))).willReturn("metadata"); - MockHttpServletRequest request = get("/saml2/metadata/" + registrationId); - Saml2MetadataResponse response = resolver.resolve(request); - assertThat(response.getMetadata()).isEqualTo("metadata"); - assertThat(response.getFileName()).isEqualTo("saml-" + registrationId + "-metadata.xml"); - verify(this.metadataFactory).resolve(any(Collection.class)); - } - - @Test - void saml2MetadataResolveWhenNoMatchingRegistrationThenNull() { - RelyingPartyRegistrationRepository registrations = mock(RelyingPartyRegistrationRepository.class); - RequestMatcherMetadataResponseResolver resolver = new RequestMatcherMetadataResponseResolver(registrations, - this.metadataFactory); - MockHttpServletRequest request = get("/saml2/metadata"); - Saml2MetadataResponse response = resolver.resolve(request); - assertThat(response).isNull(); - } - - @Test - void saml2MetadataRegistrationIdResolveWhenNoMatchingRegistrationThenException() { - RelyingPartyRegistrationRepository registrations = mock(RelyingPartyRegistrationRepository.class); - RequestMatcherMetadataResponseResolver resolver = new RequestMatcherMetadataResponseResolver(registrations, - this.metadataFactory); - MockHttpServletRequest request = get("/saml2/metadata/id"); - assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> resolver.resolve(request)); - } - - @Test - void resolveWhenNoRegistrationIdThenResolvesAll() { - RelyingPartyRegistration one = withEntityId("one"); - RelyingPartyRegistration two = withEntityId("two"); - RelyingPartyRegistrationRepository registrations = new InMemoryRelyingPartyRegistrationRepository(one, two); - RequestMatcherMetadataResponseResolver resolver = new RequestMatcherMetadataResponseResolver(registrations, - this.metadataFactory); - given(this.metadataFactory.resolve(any(Collection.class))).willReturn("metadata"); - MockHttpServletRequest request = get("/saml2/metadata"); - Saml2MetadataResponse response = resolver.resolve(request); - assertThat(response.getMetadata()).isEqualTo("metadata"); - assertThat(response.getFileName()).doesNotContain(one.getRegistrationId()) - .contains("saml") - .contains("metadata.xml"); - verify(this.metadataFactory).resolve(any(Collection.class)); - } - - @Test - void resolveWhenRequestDoesNotMatchThenNull() { - RelyingPartyRegistrationRepository registrations = mock(RelyingPartyRegistrationRepository.class); - RequestMatcherMetadataResponseResolver resolver = new RequestMatcherMetadataResponseResolver(registrations, - this.metadataFactory); - assertThat(resolver.resolve(new MockHttpServletRequest())).isNull(); - } - - // gh-13700 - @Test - void resolveWhenNoRegistrationIdThenResolvesEntityIds() { - RelyingPartyRegistration one = withEntityId("one"); - RelyingPartyRegistration two = withEntityId("two"); - RelyingPartyRegistrationRepository registrations = new InMemoryRelyingPartyRegistrationRepository(one, two); - RequestMatcherMetadataResponseResolver resolver = new RequestMatcherMetadataResponseResolver(registrations, - this.metadataFactory); - given(this.metadataFactory.resolve(any(Collection.class))).willReturn("metadata"); - resolver.resolve(get("/saml2/metadata")); - ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); - verify(this.metadataFactory).resolve(captor.capture()); - Collection resolved = captor.getValue(); - assertThat(resolved).hasSize(2); - assertThat(resolved.iterator().next().getEntityId()).isEqualTo("one"); - } - - private MockHttpServletRequest get(String uri) { - MockHttpServletRequest request = new MockHttpServletRequest("GET", uri); - request.setServletPath(uri); - return request; - } - - private RelyingPartyRegistration withEntityId(String entityId) { - return TestRelyingPartyRegistrations.relyingPartyRegistration() - .registrationId(entityId) - .entityId("{registrationId}") - .build(); - } - -} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepositoryTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepositoryTests.java deleted file mode 100644 index 22fee63c07..0000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepositoryTests.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2002-2023 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.saml2.provider.service.registration; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link InMemoryRelyingPartyRegistrationRepository} - */ -public class InMemoryRelyingPartyRegistrationRepositoryTests { - - @Test - void findByRegistrationIdWhenGivenIdThenReturnsMatchingRegistration() { - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); - InMemoryRelyingPartyRegistrationRepository registrations = new InMemoryRelyingPartyRegistrationRepository( - registration); - assertThat(registrations.findByRegistrationId(registration.getRegistrationId())).isSameAs(registration); - } - - @Test - void findByRegistrationIdWhenGivenWrongIdThenReturnsNull() { - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); - InMemoryRelyingPartyRegistrationRepository registrations = new InMemoryRelyingPartyRegistrationRepository( - registration); - assertThat(registrations.findByRegistrationId(registration.getRegistrationId() + "wrong")).isNull(); - assertThat(registrations.findByRegistrationId(null)).isNull(); - } - - @Test - void findByAssertingPartyEntityIdWhenGivenEntityIdThenReturnsMatchingRegistrations() { - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); - InMemoryRelyingPartyRegistrationRepository registrations = new InMemoryRelyingPartyRegistrationRepository( - registration); - String assertingPartyEntityId = registration.getAssertingPartyDetails().getEntityId(); - assertThat(registrations.findUniqueByAssertingPartyEntityId(assertingPartyEntityId)).isEqualTo(registration); - } - - @Test - void findByAssertingPartyEntityIdWhenGivenWrongEntityIdThenReturnsEmpty() { - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); - InMemoryRelyingPartyRegistrationRepository registrations = new InMemoryRelyingPartyRegistrationRepository( - registration); - String assertingPartyEntityId = registration.getAssertingPartyDetails().getEntityId(); - assertThat(registrations.findUniqueByAssertingPartyEntityId(assertingPartyEntityId + "wrong")).isNull(); - } - -} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverterTests.java new file mode 100644 index 0000000000..cb055622d5 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataAssertingPartyDetailsConverterTests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.registration; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.opensaml.xmlsec.signature.support.SignatureConstants; + +import org.springframework.security.saml2.Saml2Exception; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class OpenSamlMetadataAssertingPartyDetailsConverterTests { + + private static final String CERTIFICATE = "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk"; + + private static final String ENTITIES_DESCRIPTOR_TEMPLATE = "\n%s"; + + private static final String ENTITY_DESCRIPTOR_TEMPLATE = "\n%s" + + ""; + + private static final String IDP_SSO_DESCRIPTOR_TEMPLATE = "\n" + + "%s\n" + ""; + + private static final String KEY_DESCRIPTOR_TEMPLATE = "\n" + + "\n" + "\n" + + "" + CERTIFICATE + "\n" + "\n" + "\n" + + ""; + + private static final String EXTENSIONS_TEMPLATE = "" + "" + ""; + + private static final String SINGLE_SIGN_ON_SERVICE_TEMPLATE = ""; + + private OpenSamlMetadataAssertingPartyDetailsConverter converter; + + @BeforeEach + public void setup() { + this.converter = new OpenSamlMetadataAssertingPartyDetailsConverter(); + } + + @Test + public void readWhenMissingIDPSSODescriptorThenException() { + String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, ""); + InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); + assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) + .withMessageContaining("Metadata response is missing the necessary IDPSSODescriptor element"); + } + + @Test + public void readWhenMissingVerificationKeyThenException() { + String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, "")); + InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); + assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) + .withMessageContaining( + "Metadata response is missing verification certificates, necessary for verifying SAML assertions"); + } + + @Test + public void readWhenMissingSingleSignOnServiceThenException() { + String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, + String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\""))); + InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); + assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) + .withMessageContaining( + "Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests"); + } + + @Test + public void readWhenDescriptorFullySpecifiedThenConfigures() throws Exception { + String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, + String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, + String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"") + + String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") + EXTENSIONS_TEMPLATE + + String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE))); + InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); + RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream) + .iterator() + .next() + .build(); + assertThat(details.getWantAuthnRequestsSigned()).isFalse(); + assertThat(details.getSigningAlgorithms()).containsExactly(SignatureConstants.ALGO_ID_DIGEST_SHA512); + assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location"); + assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + assertThat(details.getEntityId()).isEqualTo("entity-id"); + assertThat(details.getVerificationX509Credentials()).hasSize(1); + assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate()) + .isEqualTo(x509Certificate(CERTIFICATE)); + assertThat(details.getEncryptionX509Credentials()).hasSize(1); + assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate()) + .isEqualTo(x509Certificate(CERTIFICATE)); + assertThat(details).isInstanceOf(OpenSamlAssertingPartyDetails.class); + OpenSamlAssertingPartyDetails openSamlDetails = (OpenSamlAssertingPartyDetails) details; + EntityDescriptor entityDescriptor = openSamlDetails.getEntityDescriptor(); + assertThat(entityDescriptor).isNotNull(); + assertThat(entityDescriptor.getEntityID()).isEqualTo(details.getEntityId()); + } + + // gh-9051 + @Test + public void readWhenEntitiesDescriptorThenConfigures() throws Exception { + String payload = String.format(ENTITIES_DESCRIPTOR_TEMPLATE, + String.format(ENTITY_DESCRIPTOR_TEMPLATE, + String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, + String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"") + + String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") + + String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)))); + InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); + RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream) + .iterator() + .next() + .build(); + assertThat(details.getWantAuthnRequestsSigned()).isFalse(); + assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location"); + assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + assertThat(details.getEntityId()).isEqualTo("entity-id"); + assertThat(details.getVerificationX509Credentials()).hasSize(1); + assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate()) + .isEqualTo(x509Certificate(CERTIFICATE)); + assertThat(details.getEncryptionX509Credentials()).hasSize(1); + assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate()) + .isEqualTo(x509Certificate(CERTIFICATE)); + } + + @Test + public void readWhenKeyDescriptorHasNoUseThenConfiguresBothKeyTypes() throws Exception { + String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, + String.format(KEY_DESCRIPTOR_TEMPLATE, "") + String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE))); + InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); + RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream) + .iterator() + .next() + .build(); + assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate()) + .isEqualTo(x509Certificate(CERTIFICATE)); + assertThat(details.getEncryptionX509Credentials()).hasSize(1); + assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate()) + .isEqualTo(x509Certificate(CERTIFICATE)); + } + + X509Certificate x509Certificate(String data) { + try { + InputStream certificate = new ByteArrayInputStream(Base64.getDecoder().decode(data.getBytes())); + return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certificate); + } + catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + + // gh-9051 + @Test + public void readWhenUnsupportedElementThenSaml2Exception() { + String payload = ""; + InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); + assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) + .withMessage("Unsupported element of type saml2:Assertion"); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java index ee270c8672..19ff56a575 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlMetadataRelyingPartyRegistrationConverterTests.java @@ -21,47 +21,17 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Base64; import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.opensaml.saml.saml2.metadata.EntityDescriptor; -import org.opensaml.xmlsec.signature.support.SignatureConstants; import org.springframework.core.io.ClassPathResource; -import org.springframework.security.saml2.Saml2Exception; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; public class OpenSamlMetadataRelyingPartyRegistrationConverterTests { - private static final String CERTIFICATE = "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk"; - - private static final String ENTITIES_DESCRIPTOR_TEMPLATE = "\n%s"; - - private static final String ENTITY_DESCRIPTOR_TEMPLATE = "\n%s" - + ""; - - private static final String IDP_SSO_DESCRIPTOR_TEMPLATE = "\n" - + "%s\n" + ""; - - private static final String KEY_DESCRIPTOR_TEMPLATE = "\n" - + "\n" + "\n" - + "" + CERTIFICATE + "\n" + "\n" + "\n" - + ""; - - private static final String EXTENSIONS_TEMPLATE = "" + "" + ""; - - private static final String SINGLE_SIGN_ON_SERVICE_TEMPLATE = ""; - private OpenSamlMetadataRelyingPartyRegistrationConverter converter = new OpenSamlMetadataRelyingPartyRegistrationConverter(); private String metadata; @@ -84,125 +54,4 @@ public class OpenSamlMetadataRelyingPartyRegistrationConverterTests { } } - @Test - public void readWhenMissingIDPSSODescriptorThenException() { - String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, ""); - InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); - assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) - .withMessageContaining("Metadata response is missing the necessary IDPSSODescriptor element"); - } - - @Test - public void readWhenMissingVerificationKeyThenException() { - String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, "")); - InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); - assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) - .withMessageContaining( - "Metadata response is missing verification certificates, necessary for verifying SAML assertions"); - } - - @Test - public void readWhenMissingSingleSignOnServiceThenException() { - String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, - String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\""))); - InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); - assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) - .withMessageContaining( - "Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests"); - } - - @Test - public void readWhenDescriptorFullySpecifiedThenConfigures() throws Exception { - String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, - String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, - String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"") - + String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") + EXTENSIONS_TEMPLATE - + String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE))); - InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); - RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream) - .iterator() - .next() - .build() - .getAssertingPartyDetails(); - assertThat(details.getWantAuthnRequestsSigned()).isFalse(); - assertThat(details.getSigningAlgorithms()).containsExactly(SignatureConstants.ALGO_ID_DIGEST_SHA512); - assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location"); - assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); - assertThat(details.getEntityId()).isEqualTo("entity-id"); - assertThat(details.getVerificationX509Credentials()).hasSize(1); - assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate()) - .isEqualTo(x509Certificate(CERTIFICATE)); - assertThat(details.getEncryptionX509Credentials()).hasSize(1); - assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate()) - .isEqualTo(x509Certificate(CERTIFICATE)); - assertThat(details).isInstanceOf(OpenSamlAssertingPartyDetails.class); - OpenSamlAssertingPartyDetails openSamlDetails = (OpenSamlAssertingPartyDetails) details; - EntityDescriptor entityDescriptor = openSamlDetails.getEntityDescriptor(); - assertThat(entityDescriptor).isNotNull(); - assertThat(entityDescriptor.getEntityID()).isEqualTo(details.getEntityId()); - } - - // gh-9051 - @Test - public void readWhenEntitiesDescriptorThenConfigures() throws Exception { - String payload = String.format(ENTITIES_DESCRIPTOR_TEMPLATE, - String.format(ENTITY_DESCRIPTOR_TEMPLATE, - String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, - String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"") - + String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") - + String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)))); - InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); - RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream) - .iterator() - .next() - .build() - .getAssertingPartyDetails(); - assertThat(details.getWantAuthnRequestsSigned()).isFalse(); - assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location"); - assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); - assertThat(details.getEntityId()).isEqualTo("entity-id"); - assertThat(details.getVerificationX509Credentials()).hasSize(1); - assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate()) - .isEqualTo(x509Certificate(CERTIFICATE)); - assertThat(details.getEncryptionX509Credentials()).hasSize(1); - assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate()) - .isEqualTo(x509Certificate(CERTIFICATE)); - } - - @Test - public void readWhenKeyDescriptorHasNoUseThenConfiguresBothKeyTypes() throws Exception { - String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, - String.format(KEY_DESCRIPTOR_TEMPLATE, "") + String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE))); - InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); - RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream) - .iterator() - .next() - .build() - .getAssertingPartyDetails(); - assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate()) - .isEqualTo(x509Certificate(CERTIFICATE)); - assertThat(details.getEncryptionX509Credentials()).hasSize(1); - assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate()) - .isEqualTo(x509Certificate(CERTIFICATE)); - } - - X509Certificate x509Certificate(String data) { - try { - InputStream certificate = new ByteArrayInputStream(Base64.getDecoder().decode(data.getBytes())); - return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certificate); - } - catch (Exception ex) { - throw new IllegalArgumentException(ex); - } - } - - // gh-9051 - @Test - public void readWhenUnsupportedElementThenSaml2Exception() { - String payload = ""; - InputStream inputStream = new ByteArrayInputStream(payload.getBytes()); - assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream)) - .withMessage("Unsupported element of type saml2:Assertion"); - } - } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java index f85320e932..103d04b293 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,6 @@ public class RelyingPartyRegistrationTests { public void withRelyingPartyRegistrationWorks() { RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() .nameIdFormat("format") - .authnRequestsSigned(true) .assertingPartyDetails((a) -> a.singleSignOnServiceBinding(Saml2MessageBinding.POST)) .assertingPartyDetails((a) -> a.wantAuthnRequestsSigned(false)) .assertingPartyDetails((a) -> a.signingAlgorithms((algs) -> algs.add("alg"))) @@ -40,19 +39,6 @@ public class RelyingPartyRegistrationTests { compareRegistrations(registration, copy); } - @Test - void mutateWhenInvokedThenCreatesCopy() { - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() - .nameIdFormat("format") - .assertingPartyDetails((a) -> a.singleSignOnServiceBinding(Saml2MessageBinding.POST)) - .assertingPartyDetails((a) -> a.wantAuthnRequestsSigned(false)) - .assertingPartyDetails((a) -> a.signingAlgorithms((algs) -> algs.add("alg"))) - .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT) - .build(); - RelyingPartyRegistration copy = registration.mutate().build(); - compareRegistrations(registration, copy); - } - private void compareRegistrations(RelyingPartyRegistration registration, RelyingPartyRegistration copy) { assertThat(copy.getRegistrationId()).isEqualTo(registration.getRegistrationId()).isEqualTo("simplesamlphp"); assertThat(copy.getAssertingPartyDetails().getEntityId()) @@ -87,7 +73,6 @@ public class RelyingPartyRegistrationTests { assertThat(copy.getAssertingPartyDetails().getSigningAlgorithms()) .isEqualTo(registration.getAssertingPartyDetails().getSigningAlgorithms()); assertThat(copy.getNameIdFormat()).isEqualTo(registration.getNameIdFormat()); - assertThat(copy.isAuthnRequestsSigned()).isEqualTo(registration.isAuthnRequestsSigned()); } @Test diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java index 7d89c99810..2bf8b1fb03 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationsTests.java @@ -264,7 +264,7 @@ public class RelyingPartyRegistrationsTests { public void collectionFromMetadataLocationCanHandleFederationMetadata() { Collection federationMetadataWithSkippedSPEntries = RelyingPartyRegistrations .collectionFromMetadataLocation("classpath:test-federated-metadata.xml"); - assertThat(federationMetadataWithSkippedSPEntries).hasSize(1); + assertThat(federationMetadataWithSkippedSPEntries.size()).isEqualTo(1); } @Test diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/OpenSamlAuthenticationTokenConverterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/OpenSamlAuthenticationTokenConverterTests.java deleted file mode 100644 index c737e8ff17..0000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/OpenSamlAuthenticationTokenConverterTests.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright 2002-2023 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.saml2.provider.service.web; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.time.Instant; - -import jakarta.servlet.http.HttpServletRequest; -import net.shibboleth.utilities.java.support.xml.SerializeSupport; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; -import org.opensaml.core.xml.io.Marshaller; -import org.opensaml.core.xml.io.MarshallingException; -import org.opensaml.saml.common.SignableSAMLObject; -import org.opensaml.saml.saml2.core.Response; -import org.w3c.dom.Element; - -import org.springframework.core.io.ClassPathResource; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.core.Saml2ErrorCodes; -import org.springframework.security.saml2.core.Saml2ParameterNames; -import org.springframework.security.saml2.core.Saml2Utils; -import org.springframework.security.saml2.core.TestSaml2X509Credentials; -import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken; -import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; -import org.springframework.util.StreamUtils; -import org.springframework.web.util.UriUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link OpenSamlAuthenticationTokenConverter} - */ -@ExtendWith(MockitoExtension.class) -public final class OpenSamlAuthenticationTokenConverterTests { - - @Mock - RelyingPartyRegistrationRepository registrations; - - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); - - @Test - public void convertWhenSamlResponseThenToken() { - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations); - given(this.registrations.findByRegistrationId(any())).willReturn(this.registration); - MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId()); - request.setParameter(Saml2ParameterNames.SAML_RESPONSE, - Saml2Utils.samlEncode("response".getBytes(StandardCharsets.UTF_8))); - Saml2AuthenticationToken token = converter.convert(request); - assertThat(token.getSaml2Response()).isEqualTo("response"); - assertThat(token.getRelyingPartyRegistration().getRegistrationId()) - .isEqualTo(this.registration.getRegistrationId()); - } - - @Test - public void convertWhenSamlResponseInvalidBase64ThenSaml2AuthenticationException() { - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations); - given(this.registrations.findByRegistrationId(any())).willReturn(this.registration); - MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId()); - request.setParameter(Saml2ParameterNames.SAML_RESPONSE, "invalid"); - assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> converter.convert(request)) - .withCauseInstanceOf(IllegalArgumentException.class) - .satisfies( - (ex) -> assertThat(ex.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE)) - .satisfies( - (ex) -> assertThat(ex.getSaml2Error().getDescription()).isEqualTo("Failed to decode SAMLResponse")); - } - - @Test - public void convertWhenNoSamlResponseThenNull() { - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations); - MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId()); - assertThat(converter.convert(request)).isNull(); - } - - @Test - public void convertWhenNoMatchingRequestThenNull() { - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setParameter(Saml2ParameterNames.SAML_RESPONSE, "ignored"); - assertThat(converter.convert(request)).isNull(); - } - - @Test - public void convertWhenNoRelyingPartyRegistrationThenNull() { - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations); - MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId()); - String response = Saml2Utils.samlEncode(serialize(signed(response())).getBytes(StandardCharsets.UTF_8)); - request.setParameter(Saml2ParameterNames.SAML_RESPONSE, response); - assertThat(converter.convert(request)).isNull(); - } - - @Test - public void convertWhenGetRequestThenInflates() { - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations); - given(this.registrations.findByRegistrationId(any())).willReturn(this.registration); - MockHttpServletRequest request = get("/login/saml2/sso/" + this.registration.getRegistrationId()); - byte[] deflated = Saml2Utils.samlDeflate("response"); - String encoded = Saml2Utils.samlEncode(deflated); - request.setParameter(Saml2ParameterNames.SAML_RESPONSE, encoded); - Saml2AuthenticationToken token = converter.convert(request); - assertThat(token.getSaml2Response()).isEqualTo("response"); - assertThat(token.getRelyingPartyRegistration().getRegistrationId()) - .isEqualTo(this.registration.getRegistrationId()); - } - - @Test - public void convertWhenGetRequestInvalidDeflatedThenSaml2AuthenticationException() { - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations); - given(this.registrations.findByRegistrationId(any())).willReturn(this.registration); - MockHttpServletRequest request = get("/login/saml2/sso/" + this.registration.getRegistrationId()); - byte[] invalidDeflated = "invalid".getBytes(); - String encoded = Saml2Utils.samlEncode(invalidDeflated); - request.setParameter(Saml2ParameterNames.SAML_RESPONSE, encoded); - assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> converter.convert(request)) - .withCauseInstanceOf(IOException.class) - .satisfies( - (ex) -> assertThat(ex.getSaml2Error().getErrorCode()).isEqualTo(Saml2ErrorCodes.INVALID_RESPONSE)) - .satisfies((ex) -> assertThat(ex.getSaml2Error().getDescription()).isEqualTo("Unable to inflate string")); - } - - @Test - public void convertWhenUsingSamlUtilsBase64ThenXmlIsValid() throws Exception { - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations); - given(this.registrations.findByRegistrationId(any())).willReturn(this.registration); - MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId()); - request.setParameter(Saml2ParameterNames.SAML_RESPONSE, getSsoCircleEncodedXml()); - Saml2AuthenticationToken token = converter.convert(request); - validateSsoCircleXml(token.getSaml2Response()); - } - - @Test - public void convertWhenSavedAuthenticationRequestThenToken() { - Saml2AuthenticationRequestRepository authenticationRequestRepository = mock( - Saml2AuthenticationRequestRepository.class); - AbstractSaml2AuthenticationRequest authenticationRequest = mock(AbstractSaml2AuthenticationRequest.class); - given(authenticationRequest.getRelyingPartyRegistrationId()).willReturn(this.registration.getRegistrationId()); - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations); - converter.setAuthenticationRequestRepository(authenticationRequestRepository); - given(this.registrations.findByRegistrationId(any())).willReturn(this.registration); - given(authenticationRequestRepository.loadAuthenticationRequest(any(HttpServletRequest.class))) - .willReturn(authenticationRequest); - MockHttpServletRequest request = post("/login/saml2/sso/" + this.registration.getRegistrationId()); - request.setParameter(Saml2ParameterNames.SAML_RESPONSE, - Saml2Utils.samlEncode("response".getBytes(StandardCharsets.UTF_8))); - Saml2AuthenticationToken token = converter.convert(request); - assertThat(token.getSaml2Response()).isEqualTo("response"); - assertThat(token.getRelyingPartyRegistration().getRegistrationId()) - .isEqualTo(this.registration.getRegistrationId()); - assertThat(token.getAuthenticationRequest()).isEqualTo(authenticationRequest); - } - - @Test - public void convertWhenMatchingNoRegistrationIdThenLooksUpByAssertingEntityId() { - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations); - String response = serialize(signed(response())); - String encoded = Saml2Utils.samlEncode(response.getBytes(StandardCharsets.UTF_8)); - given(this.registrations.findUniqueByAssertingPartyEntityId(TestOpenSamlObjects.ASSERTING_PARTY_ENTITY_ID)) - .willReturn(this.registration); - MockHttpServletRequest request = post("/login/saml2/sso"); - request.setParameter(Saml2ParameterNames.SAML_RESPONSE, encoded); - Saml2AuthenticationToken token = converter.convert(request); - assertThat(token.getSaml2Response()).isEqualTo(response); - assertThat(token.getRelyingPartyRegistration().getRegistrationId()) - .isEqualTo(this.registration.getRegistrationId()); - } - - @Test - public void constructorWhenResolverIsNullThenIllegalArgument() { - assertThatIllegalArgumentException().isThrownBy(() -> new Saml2AuthenticationTokenConverter(null)); - } - - @Test - public void setAuthenticationRequestRepositoryWhenNullThenIllegalArgument() { - OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(this.registrations); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> converter.setAuthenticationRequestRepository(null)); - } - - private void validateSsoCircleXml(String xml) { - assertThat(xml).contains("InResponseTo=\"ARQ9a73ead-7dcf-45a8-89eb-26f3c9900c36\"") - .contains(" ID=\"s246d157446618e90e43fb79bdd4d9e9e19cf2c7c4\"") - .contains("https://idp.ssocircle.com"); - } - - private String getSsoCircleEncodedXml() throws IOException { - ClassPathResource resource = new ClassPathResource("saml2-response-sso-circle.encoded"); - String response = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8); - return UriUtils.decode(response, StandardCharsets.UTF_8); - } - - private MockHttpServletRequest post(String uri) { - MockHttpServletRequest request = new MockHttpServletRequest("POST", uri); - request.setServletPath(uri); - return request; - } - - private MockHttpServletRequest get(String uri) { - MockHttpServletRequest request = new MockHttpServletRequest("GET", uri); - request.setServletPath(uri); - return request; - } - - private T signed(T toSign) { - TestOpenSamlObjects.signed(toSign, TestSaml2X509Credentials.assertingPartySigningCredential(), - TestOpenSamlObjects.RELYING_PARTY_ENTITY_ID); - return toSign; - } - - private Response response() { - Response response = TestOpenSamlObjects.response(); - response.setIssueInstant(Instant.now()); - return response; - } - - private String serialize(XMLObject object) { - try { - Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object); - Element element = marshaller.marshall(object); - return SerializeSupport.nodeToString(element); - } - catch (MarshallingException ex) { - throw new Saml2Exception(ex); - } - } - -} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationPlaceholderResolversTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationPlaceholderResolversTests.java deleted file mode 100644 index 911905c93c..0000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationPlaceholderResolversTests.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2002-2023 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.saml2.provider.service.web; - -import org.junit.jupiter.api.Test; - -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Tests for {@link RelyingPartyRegistrationPlaceholderResolvers} - */ -public class RelyingPartyRegistrationPlaceholderResolversTests { - - @Test - void uriResolverGivenRequestCreatesResolver() { - MockHttpServletRequest request = new MockHttpServletRequest(); - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request); - String resolved = uriResolver.resolve("{baseUrl}/extension"); - assertThat(resolved).isEqualTo("http://localhost/extension"); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> uriResolver.resolve("{baseUrl}/extension/{registrationId}")); - } - - @Test - void uriResolverGivenRequestAndRegistrationCreatesResolver() { - MockHttpServletRequest request = new MockHttpServletRequest(); - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() - .entityId("http://sp.example.org") - .build(); - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); - String resolved = uriResolver.resolve("{baseUrl}/extension/{registrationId}"); - assertThat(resolved).isEqualTo("http://localhost/extension/simplesamlphp"); - resolved = uriResolver.resolve("{relyingPartyEntityId}/extension"); - assertThat(resolved).isEqualTo("http://sp.example.org/extension"); - } - -} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java index b24dfd2023..e046745d1b 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java @@ -64,7 +64,9 @@ public class Saml2MetadataFilterTests { public void setup() { this.repository = mock(RelyingPartyRegistrationRepository.class); this.resolver = mock(Saml2MetadataResolver.class); - this.filter = new Saml2MetadataFilter(this.repository, this.resolver); + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = new DefaultRelyingPartyRegistrationResolver( + this.repository); + this.filter = new Saml2MetadataFilter(relyingPartyRegistrationResolver, this.resolver); this.request = new MockHttpServletRequest(); this.response = new MockHttpServletResponse(); this.chain = mock(FilterChain.class); @@ -137,10 +139,10 @@ public class Saml2MetadataFilterTests { } @Test - public void doFilterWhenResolverConstructorAndPathStartsWithRegistrationIdThenServesMetadata() throws Exception { + public void doFilterWhenPathStartsWithRegistrationIdThenServesMetadata() throws Exception { RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); given(this.repository.findByRegistrationId("registration-id")).willReturn(registration); - given(this.resolver.resolve(any(RelyingPartyRegistration.class))).willReturn("metadata"); + given(this.resolver.resolve(any())).willReturn("metadata"); RelyingPartyRegistrationResolver resolver = new DefaultRelyingPartyRegistrationResolver( (id) -> this.repository.findByRegistrationId("registration-id")); this.filter = new Saml2MetadataFilter(resolver, this.resolver); @@ -150,20 +152,6 @@ public class Saml2MetadataFilterTests { verify(this.repository).findByRegistrationId("registration-id"); } - @Test - public void doFilterWhenRelyingPartyRegistrationRepositoryConstructorAndPathStartsWithRegistrationIdThenServesMetadata() - throws Exception { - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); - given(this.repository.findByRegistrationId("registration-id")).willReturn(registration); - given(this.resolver.resolve(any(RelyingPartyRegistration.class))).willReturn("metadata"); - this.filter = new Saml2MetadataFilter((id) -> this.repository.findByRegistrationId("registration-id"), - this.resolver); - this.filter.setRequestMatcher(new AntPathRequestMatcher("/metadata")); - this.request.setPathInfo("/metadata"); - this.filter.doFilter(this.request, this.response, new MockFilterChain()); - verify(this.repository).findByRegistrationId("registration-id"); - } - // gh-12026 @Test public void doFilterWhenCharacterEncodingThenEncodeSpecialCharactersCorrectly() throws Exception { @@ -199,13 +187,4 @@ public class Saml2MetadataFilterTests { .withMessage("metadataFilename must contain a {registrationId} match variable"); } - @Test - public void constructorWhenRelyingPartyRegistrationRepositoryThenUses() throws Exception { - RelyingPartyRegistrationRepository repository = mock(RelyingPartyRegistrationRepository.class); - this.filter = new Saml2MetadataFilter(repository, this.resolver); - this.request.setPathInfo("/saml2/service-provider-metadata/one"); - this.filter.doFilter(this.request, this.response, this.chain); - verify(repository).findByRegistrationId("one"); - } - } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolverTests.java index b4088bac63..c4ba2525be 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolverTests.java @@ -16,13 +16,8 @@ package org.springframework.security.saml2.provider.service.web.authentication; -import java.util.stream.Stream; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Answers; import org.mockito.MockedStatic; import org.opensaml.xmlsec.signature.support.SignatureConstants; @@ -37,8 +32,6 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2R import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -61,27 +54,21 @@ public class OpenSamlAuthenticationRequestResolverTests { this.relyingPartyRegistrationBuilder = TestRelyingPartyRegistrations.relyingPartyRegistration(); } - @ParameterizedTest - @MethodSource("provideSignRequestFlags") - public void resolveAuthenticationRequestWhenSignedRedirectThenSignsAndRedirects(boolean wantAuthRequestsSigned, - boolean authnRequestsSigned) { + @Test + public void resolveAuthenticationRequestWhenSignedRedirectThenSignsAndRedirects() { MockHttpServletRequest request = new MockHttpServletRequest(); request.setPathInfo("/saml2/authenticate/registration-id"); - RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder - .authnRequestsSigned(authnRequestsSigned) - .assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(wantAuthRequestsSigned)) - .build(); + RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder.build(); OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> { - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat()); assertThat(authnRequest.getAssertionConsumerServiceURL()) - .isEqualTo(uriResolver.resolve(registration.getAssertionConsumerServiceLocation())); + .isEqualTo(registration.getAssertionConsumerServiceLocation()); assertThat(authnRequest.getProtocolBinding()) .isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn()); assertThat(authnRequest.getDestination()) .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); - assertThat(authnRequest.getIssuer().getValue()).isEqualTo(uriResolver.resolve(registration.getEntityId())); + assertThat(authnRequest.getIssuer().getValue()).isEqualTo(registration.getEntityId()); }); assertThat(result.getSamlRequest()).isNotEmpty(); assertThat(result.getRelayState()).isNotNull(); @@ -100,15 +87,14 @@ public class OpenSamlAuthenticationRequestResolverTests { .build(); OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> { - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat()); assertThat(authnRequest.getAssertionConsumerServiceURL()) - .isEqualTo(uriResolver.resolve(registration.getAssertionConsumerServiceLocation())); + .isEqualTo(registration.getAssertionConsumerServiceLocation()); assertThat(authnRequest.getProtocolBinding()) .isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn()); assertThat(authnRequest.getDestination()) .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); - assertThat(authnRequest.getIssuer().getValue()).isEqualTo(uriResolver.resolve(registration.getEntityId())); + assertThat(authnRequest.getIssuer().getValue()).isEqualTo(registration.getEntityId()); }); assertThat(result.getSamlRequest()).isNotEmpty(); assertThat(result.getRelayState()).isNotNull(); @@ -138,19 +124,17 @@ public class OpenSamlAuthenticationRequestResolverTests { request.setPathInfo("/saml2/authenticate/registration-id"); RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder.assertingPartyDetails( (party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST).wantAuthnRequestsSigned(false)) - .authnRequestsSigned(false) .build(); OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> { - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat()); assertThat(authnRequest.getAssertionConsumerServiceURL()) - .isEqualTo(uriResolver.resolve(registration.getAssertionConsumerServiceLocation())); + .isEqualTo(registration.getAssertionConsumerServiceLocation()); assertThat(authnRequest.getProtocolBinding()) .isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn()); assertThat(authnRequest.getDestination()) .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); - assertThat(authnRequest.getIssuer().getValue()).isEqualTo(uriResolver.resolve(registration.getEntityId())); + assertThat(authnRequest.getIssuer().getValue()).isEqualTo(registration.getEntityId()); }); assertThat(result.getSamlRequest()).isNotEmpty(); assertThat(result.getRelayState()).isNotNull(); @@ -159,28 +143,23 @@ public class OpenSamlAuthenticationRequestResolverTests { assertThat(result.getId()).isNotEmpty(); } - @ParameterizedTest - @MethodSource("provideSignRequestFlags") - public void resolveAuthenticationRequestWhenSignedPostThenSignsAndPosts(boolean wantAuthRequestsSigned, - boolean authnRequestsSigned) { + @Test + public void resolveAuthenticationRequestWhenSignedPostThenSignsAndPosts() { MockHttpServletRequest request = new MockHttpServletRequest(); request.setPathInfo("/saml2/authenticate/registration-id"); RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder - .authnRequestsSigned(authnRequestsSigned) - .assertingPartyDetails((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST) - .wantAuthnRequestsSigned(wantAuthRequestsSigned)) + .assertingPartyDetails((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST)) .build(); OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> { - UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); assertThat(authnRequest.getNameIDPolicy().getFormat()).isEqualTo(registration.getNameIdFormat()); assertThat(authnRequest.getAssertionConsumerServiceURL()) - .isEqualTo(uriResolver.resolve(registration.getAssertionConsumerServiceLocation())); + .isEqualTo(registration.getAssertionConsumerServiceLocation()); assertThat(authnRequest.getProtocolBinding()) .isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn()); assertThat(authnRequest.getDestination()) .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); - assertThat(authnRequest.getIssuer().getValue()).isEqualTo(uriResolver.resolve(registration.getEntityId())); + assertThat(authnRequest.getIssuer().getValue()).isEqualTo(registration.getEntityId()); }); assertThat(result.getSamlRequest()).isNotEmpty(); assertThat(result.getRelayState()).isNotNull(); @@ -263,8 +242,4 @@ public class OpenSamlAuthenticationRequestResolverTests { return new OpenSamlAuthenticationRequestResolver((request, id) -> registration); } - private static Stream provideSignRequestFlags() { - return Stream.of(Arguments.of(true, true), Arguments.of(true, false), Arguments.of(false, true)); - } - } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilterTests.java index 7b3abddf95..29537b3b6f 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilterTests.java @@ -17,6 +17,7 @@ package org.springframework.security.saml2.provider.service.web.authentication; import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -44,7 +45,6 @@ import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNoException; @@ -93,7 +93,7 @@ public class Saml2WebSsoAuthenticationFilterTests { @Test public void requiresAuthenticationWhenHappyPathThenReturnsTrue() { - assertThat(this.filter.requiresAuthentication(this.request, this.response)).isTrue(); + Assertions.assertTrue(this.filter.requiresAuthentication(this.request, this.response)); } @Test @@ -101,7 +101,7 @@ public class Saml2WebSsoAuthenticationFilterTests { this.filter = new Saml2WebSsoAuthenticationFilter(this.repository, "/some/other/path/{registrationId}"); this.request.setPathInfo("/some/other/path/idp-registration-id"); this.request.setParameter(Saml2ParameterNames.SAML_RESPONSE, "xml-data-goes-here"); - assertThat(this.filter.requiresAuthentication(this.request, this.response)).isTrue(); + Assertions.assertTrue(this.filter.requiresAuthentication(this.request, this.response)); } @Test @@ -142,7 +142,7 @@ public class Saml2WebSsoAuthenticationFilterTests { this.filter.setAuthenticationDetailsSource(authenticationDetailsSource); this.request.setPathInfo("/some/other/path/idp-registration-id"); this.filter.attemptAuthentication(this.request, this.response); - assertThat(token.getDetails()).isEqualTo(details); + Assertions.assertEquals(details, token.getDetails()); } @Test diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java index e25e8b56ea..f9f22f8262 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,8 @@ package org.springframework.security.saml2.provider.service.web.authentication.logout; import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.core.convert.converter.Converter; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; @@ -34,61 +32,35 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link OpenSaml4LogoutRequestResolver} */ public class OpenSaml4LogoutRequestResolverTests { - RelyingPartyRegistration registration; - - RelyingPartyRegistrationResolver registrationResolver; - - OpenSaml4LogoutRequestResolver logoutRequestResolver; - - @BeforeEach - public void setup() { - this.registration = TestRelyingPartyRegistrations.full().build(); - this.registrationResolver = mock(RelyingPartyRegistrationResolver.class); - this.logoutRequestResolver = new OpenSaml4LogoutRequestResolver(this.registrationResolver); - } + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class); @Test public void resolveWhenCustomParametersConsumerThenUses() { - this.logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid")); - given(this.registrationResolver.resolve(any(), any())).willReturn(this.registration); - - Saml2LogoutRequest logoutRequest = this.logoutRequestResolver.resolve(givenRequest(), givenAuthentication()); - + OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver( + this.relyingPartyRegistrationResolver); + logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid")); + HttpServletRequest request = new MockHttpServletRequest(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .assertingPartyDetails((party) -> party.singleLogoutServiceLocation("https://ap.example.com/logout")) + .build(); + Authentication authentication = new TestingAuthenticationToken("user", "password"); + given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration); + Saml2LogoutRequest logoutRequest = logoutRequestResolver.resolve(request, authentication); assertThat(logoutRequest.getId()).isEqualTo("myid"); } @Test public void setParametersConsumerWhenNullThenIllegalArgument() { + OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver( + this.relyingPartyRegistrationResolver); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> this.logoutRequestResolver.setParametersConsumer(null)); - } - - @Test - public void resolveWhenCustomRelayStateThenUses() { - given(this.registrationResolver.resolve(any(), any())).willReturn(this.registration); - Converter relayState = mock(Converter.class); - given(relayState.convert(any())).willReturn("any-state"); - this.logoutRequestResolver.setRelayStateResolver(relayState); - - Saml2LogoutRequest logoutRequest = this.logoutRequestResolver.resolve(givenRequest(), givenAuthentication()); - - assertThat(logoutRequest.getRelayState()).isEqualTo("any-state"); - verify(relayState).convert(any()); - } - - private static Authentication givenAuthentication() { - return new TestingAuthenticationToken("user", "password"); - } - - private MockHttpServletRequest givenRequest() { - return new MockHttpServletRequest(); + .isThrownBy(() -> logoutRequestResolver.setParametersConsumer(null)); } } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java index 27c1ad267c..7e8352c6a5 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolverTests.java @@ -88,7 +88,7 @@ public class OpenSamlLogoutRequestResolverTests { LogoutRequest logoutRequest = getLogoutRequest(saml2LogoutRequest.getSamlRequest(), binding); assertThat(logoutRequest.getNameID().getValue()).isEqualTo(authentication.getName()); assertThat(logoutRequest.getSessionIndexes()).hasSize(1); - assertThat(logoutRequest.getSessionIndexes().get(0).getValue()).isEqualTo("session-index"); + assertThat(logoutRequest.getSessionIndexes().get(0).getSessionIndex()).isEqualTo("session-index"); } private Saml2Authentication authentication(RelyingPartyRegistration registration) { diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolverTests.java deleted file mode 100644 index 8e2ae5a393..0000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestValidatorParametersResolverTests.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2002-2023 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.saml2.provider.service.web.authentication.logout; - -import java.nio.charset.StandardCharsets; - -import net.shibboleth.utilities.java.support.xml.SerializeSupport; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; -import org.opensaml.core.xml.io.Marshaller; -import org.opensaml.core.xml.io.MarshallingException; -import org.w3c.dom.Element; - -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.core.Saml2ParameterNames; -import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; -import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; -import org.springframework.security.saml2.provider.service.authentication.TestSaml2Authentications; -import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidatorParameters; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.BDDMockito.given; - -@ExtendWith(MockitoExtension.class) -public final class OpenSamlLogoutRequestValidatorParametersResolverTests { - - @Mock - RelyingPartyRegistrationRepository registrations; - - private RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); - - private OpenSamlLogoutRequestValidatorParametersResolver resolver; - - @BeforeEach - void setup() { - this.resolver = new OpenSamlLogoutRequestValidatorParametersResolver(this.registrations); - } - - @Test - void saml2LogoutRegistrationIdResolveWhenMatchesThenParameters() { - String registrationId = this.registration.getRegistrationId(); - MockHttpServletRequest request = post("/logout/saml2/slo/" + registrationId); - Authentication authentication = new TestingAuthenticationToken("user", "pass"); - request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request"); - given(this.registrations.findByRegistrationId(registrationId)).willReturn(this.registration); - Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, authentication); - assertThat(parameters.getAuthentication()).isEqualTo(authentication); - assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId); - assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo("request"); - } - - @Test - void saml2LogoutRegistrationIdWhenUnauthenticatedThenParameters() { - String registrationId = this.registration.getRegistrationId(); - MockHttpServletRequest request = post("/logout/saml2/slo/" + registrationId); - request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request"); - given(this.registrations.findByRegistrationId(registrationId)).willReturn(this.registration); - Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, null); - assertThat(parameters.getAuthentication()).isNull(); - assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId); - assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo("request"); - } - - @Test - void saml2LogoutResolveWhenAuthenticatedThenParameters() { - String registrationId = this.registration.getRegistrationId(); - MockHttpServletRequest request = post("/logout/saml2/slo"); - Authentication authentication = TestSaml2Authentications.authentication(); - request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request"); - given(this.registrations.findByRegistrationId(registrationId)).willReturn(this.registration); - Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, authentication); - assertThat(parameters.getAuthentication()).isEqualTo(authentication); - assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId); - assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo("request"); - } - - @Test - void saml2LogoutResolveWhenUnauthenticatedThenParameters() { - String registrationId = this.registration.getRegistrationId(); - MockHttpServletRequest request = post("/logout/saml2/slo"); - String logoutRequest = serialize(TestOpenSamlObjects.logoutRequest()); - String encoded = Saml2Utils.samlEncode(logoutRequest.getBytes(StandardCharsets.UTF_8)); - request.setParameter(Saml2ParameterNames.SAML_REQUEST, encoded); - given(this.registrations.findUniqueByAssertingPartyEntityId(TestOpenSamlObjects.ASSERTING_PARTY_ENTITY_ID)) - .willReturn(this.registration); - Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, null); - assertThat(parameters.getAuthentication()).isNull(); - assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId); - assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo(encoded); - } - - @Test - void saml2LogoutResolveWhenUnauthenticatedGetRequestThenInflates() { - String registrationId = this.registration.getRegistrationId(); - MockHttpServletRequest request = get("/logout/saml2/slo"); - String logoutRequest = serialize(TestOpenSamlObjects.logoutRequest()); - String encoded = Saml2Utils.samlEncode(Saml2Utils.samlDeflate(logoutRequest)); - request.setParameter(Saml2ParameterNames.SAML_REQUEST, encoded); - given(this.registrations.findUniqueByAssertingPartyEntityId(TestOpenSamlObjects.ASSERTING_PARTY_ENTITY_ID)) - .willReturn(this.registration); - Saml2LogoutRequestValidatorParameters parameters = this.resolver.resolve(request, null); - assertThat(parameters.getAuthentication()).isNull(); - assertThat(parameters.getRelyingPartyRegistration().getRegistrationId()).isEqualTo(registrationId); - assertThat(parameters.getLogoutRequest().getSamlRequest()).isEqualTo(encoded); - } - - @Test - void saml2LogoutRegistrationIdResolveWhenNoMatchingRegistrationIdThenSaml2Exception() { - MockHttpServletRequest request = post("/logout/saml2/slo/id"); - request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request"); - assertThatExceptionOfType(Saml2AuthenticationException.class) - .isThrownBy(() -> this.resolver.resolve(request, null)); - } - - private MockHttpServletRequest post(String uri) { - MockHttpServletRequest request = new MockHttpServletRequest("POST", uri); - request.setServletPath(uri); - return request; - } - - private MockHttpServletRequest get(String uri) { - MockHttpServletRequest request = new MockHttpServletRequest("GET", uri); - request.setServletPath(uri); - return request; - } - - private String serialize(XMLObject object) { - try { - Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object); - Element element = marshaller.marshall(object); - return SerializeSupport.nodeToString(element); - } - catch (MarshallingException ex) { - throw new Saml2Exception(ex); - } - } - -} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java index eb640c470d..bae0509b63 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolverTests.java @@ -56,7 +56,7 @@ public class OpenSamlLogoutResponseResolverTests { RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = mock(RelyingPartyRegistrationResolver.class); - OpenSamlLogoutResponseResolver logoutResponseResolver = new OpenSamlLogoutResponseResolver(null, + OpenSamlLogoutResponseResolver logoutResponseResolver = new OpenSamlLogoutResponseResolver( this.relyingPartyRegistrationResolver); @Test diff --git a/settings.gradle b/settings.gradle index cb9c8bb8a0..b5ecec8d39 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,14 +5,13 @@ pluginManagement { } plugins { - id "com.gradle.enterprise" version "3.12.6" + id "com.gradle.enterprise" version "3.11.4" id "io.spring.ge.conventions" version "0.0.14" } dependencyResolutionManagement { repositories { mavenCentral() - maven { url "https://repo.spring.io/milestone" } } } diff --git a/taglibs/src/main/resources/META-INF/security.tld b/taglibs/src/main/resources/META-INF/security.tld index 5aed63096f..0ae747b7df 100644 --- a/taglibs/src/main/resources/META-INF/security.tld +++ b/taglibs/src/main/resources/META-INF/security.tld @@ -20,7 +20,7 @@ version="2.0"> Spring Security Authorization Tag Library - 6.2 + 6.0 security http://www.springframework.org/security/tags diff --git a/test/spring-security-test.gradle b/test/spring-security-test.gradle index 15d07dd367..92b3868438 100644 --- a/test/spring-security-test.gradle +++ b/test/spring-security-test.gradle @@ -27,6 +27,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-params" testImplementation "org.junit.jupiter:junit-jupiter-engine" testImplementation "org.mockito:mockito-core" + testImplementation 'org.mockito:mockito-inline' testImplementation "org.mockito:mockito-junit-jupiter" testImplementation "org.springframework:spring-test" testImplementation 'org.skyscreamer:jsonassert' diff --git a/test/src/main/java/org/springframework/security/test/context/support/WithAnonymousUserSecurityContextFactory.java b/test/src/main/java/org/springframework/security/test/context/support/WithAnonymousUserSecurityContextFactory.java index f366a89cbd..28cc347410 100644 --- a/test/src/main/java/org/springframework/security/test/context/support/WithAnonymousUserSecurityContextFactory.java +++ b/test/src/main/java/org/springframework/security/test/context/support/WithAnonymousUserSecurityContextFactory.java @@ -28,7 +28,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; /** - * A {@link WithSecurityContextFactory} that runs with an + * A {@link WithAnonymousUserSecurityContextFactory} that runs with an * {@link AnonymousAuthenticationToken}. . * * @author Rob Winch diff --git a/test/src/main/java/org/springframework/security/test/context/support/WithMockUserSecurityContextFactory.java b/test/src/main/java/org/springframework/security/test/context/support/WithMockUserSecurityContextFactory.java index f4717a72a3..1296cca9e5 100644 --- a/test/src/main/java/org/springframework/security/test/context/support/WithMockUserSecurityContextFactory.java +++ b/test/src/main/java/org/springframework/security/test/context/support/WithMockUserSecurityContextFactory.java @@ -33,7 +33,7 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * A {@link WithSecurityContextFactory} that works with {@link WithMockUser}. + * A {@link WithUserDetailsSecurityContextFactory} that works with {@link WithMockUser}. * * @author Rob Winch * @since 4.0 diff --git a/test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java b/test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java index 7948b9e87b..395dd53bb1 100644 --- a/test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java +++ b/test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java @@ -34,7 +34,8 @@ import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** - * A {@link WithSecurityContextFactory} that works with {@link WithUserDetails} . + * A {@link WithUserDetailsSecurityContextFactory} that works with {@link WithUserDetails} + * . * * @author Rob Winch * @since 4.0 diff --git a/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurerOpaqueTokenTests.java b/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurerOpaqueTokenTests.java index 8fb1168c30..b0c921462b 100644 --- a/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurerOpaqueTokenTests.java +++ b/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurerOpaqueTokenTests.java @@ -66,7 +66,7 @@ public class SecurityMockServerConfigurerOpaqueTokenTests extends AbstractMockSe BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication(); assertThat(token.getAuthorities()).isNotEmpty(); assertThat(token.getToken()).isNotNull(); - assertThat(token.getTokenAttributes()).containsEntry(OAuth2TokenIntrospectionClaimNames.SUB, "user"); + assertThat(token.getTokenAttributes().get(OAuth2TokenIntrospectionClaimNames.SUB)).isEqualTo("user"); } @Test diff --git a/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurersJwtTests.java b/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurersJwtTests.java index ad43313e00..b8d1d2ddd5 100644 --- a/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurersJwtTests.java +++ b/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurersJwtTests.java @@ -65,7 +65,7 @@ public class SecurityMockServerConfigurersJwtTests extends AbstractMockServerCon assertThat(token.getAuthorities()).isNotEmpty(); assertThat(token.getToken()).isNotNull(); assertThat(token.getToken().getSubject()).isEqualTo("user"); - assertThat(token.getToken().getHeaders()).containsEntry("alg", "none"); + assertThat(token.getToken().getHeaders().get("alg")).isEqualTo("none"); } @Test @@ -137,7 +137,7 @@ public class SecurityMockServerConfigurersJwtTests extends AbstractMockServerCon JwtAuthenticationToken retrievedToken = (JwtAuthenticationToken) context.getAuthentication(); assertThat(retrievedToken.getToken().getSubject()).isEqualTo("some_user"); assertThat(retrievedToken.getToken().getTokenValue()).isEqualTo("token"); - assertThat(retrievedToken.getToken().getHeaders()).containsEntry("header1", "value1"); + assertThat(retrievedToken.getToken().getHeaders().get("header1")).isEqualTo("value1"); } } diff --git a/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsJwtTests.java b/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsJwtTests.java index 03192ac4ec..b6726b0b66 100644 --- a/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsJwtTests.java +++ b/test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsJwtTests.java @@ -102,7 +102,7 @@ public class SecurityMockMvcRequestPostProcessorsJwtTests { assertThat(token.getAuthorities()).isNotEmpty(); assertThat(token.getToken()).isNotNull(); assertThat(token.getToken().getSubject()).isEqualTo("user"); - assertThat(token.getToken().getHeaders()).containsEntry("alg", "none"); + assertThat(token.getToken().getHeaders().get("alg")).isEqualTo("none"); } @Test @@ -160,7 +160,7 @@ public class SecurityMockMvcRequestPostProcessorsJwtTests { JwtAuthenticationToken retrievedToken = (JwtAuthenticationToken) context.getAuthentication(); assertThat(retrievedToken.getToken().getSubject()).isEqualTo("some_user"); assertThat(retrievedToken.getToken().getTokenValue()).isEqualTo("token"); - assertThat(retrievedToken.getToken().getHeaders()).containsEntry("header1", "value1"); + assertThat(retrievedToken.getToken().getHeaders().get("header1")).isEqualTo("value1"); } } diff --git a/test/src/test/java/org/springframework/security/test/web/servlet/response/Gh3409Tests.java b/test/src/test/java/org/springframework/security/test/web/servlet/response/Gh3409Tests.java index 1e40bf04d2..b8e193e88e 100644 --- a/test/src/test/java/org/springframework/security/test/web/servlet/response/Gh3409Tests.java +++ b/test/src/test/java/org/springframework/security/test/web/servlet/response/Gh3409Tests.java @@ -78,7 +78,7 @@ public class Gh3409Tests { } @Test - public void unauthenticatedNullAuthentication() throws Exception { + public void unauthenticatedNullAuthenitcation() throws Exception { // @formatter:off this.mockMvc .perform(get("/") diff --git a/web/spring-security-web.gradle b/web/spring-security-web.gradle index cf405549c2..c0ee717cf0 100644 --- a/web/spring-security-web.gradle +++ b/web/spring-security-web.gradle @@ -26,6 +26,7 @@ dependencies { testImplementation 'jakarta.websocket:jakarta.websocket-client-api' testImplementation 'org.hamcrest:hamcrest' testImplementation 'org.mockito:mockito-core' + testImplementation 'org.mockito:mockito-inline' testImplementation 'org.skyscreamer:jsonassert' testImplementation 'org.springframework:spring-webflux' testImplementation 'org.synchronoss.cloud:nio-multipart-parser' diff --git a/web/src/main/java/org/springframework/security/web/DefaultRedirectStrategy.java b/web/src/main/java/org/springframework/security/web/DefaultRedirectStrategy.java index ff74b38346..8e9f0def76 100644 --- a/web/src/main/java/org/springframework/security/web/DefaultRedirectStrategy.java +++ b/web/src/main/java/org/springframework/security/web/DefaultRedirectStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -24,8 +24,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.log.LogMessage; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.security.web.util.UrlUtils; import org.springframework.util.Assert; @@ -34,7 +32,6 @@ import org.springframework.util.Assert; * the framework. * * @author Luke Taylor - * @author Mark Chesney * @since 3.0 */ public class DefaultRedirectStrategy implements RedirectStrategy { @@ -43,8 +40,6 @@ public class DefaultRedirectStrategy implements RedirectStrategy { private boolean contextRelative; - private HttpStatus statusCode = HttpStatus.FOUND; - /** * Redirects the response to the supplied URL. *

      @@ -60,14 +55,7 @@ public class DefaultRedirectStrategy implements RedirectStrategy { if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Redirecting to %s", redirectUrl)); } - if (this.statusCode == HttpStatus.FOUND) { - response.sendRedirect(redirectUrl); - } - else { - response.setHeader(HttpHeaders.LOCATION, redirectUrl); - response.setStatus(this.statusCode.value()); - response.getWriter().flush(); - } + response.sendRedirect(redirectUrl); } protected String calculateRedirectUrl(String contextPath, String url) { @@ -108,18 +96,4 @@ public class DefaultRedirectStrategy implements RedirectStrategy { return this.contextRelative; } - /** - * Sets the HTTP status code to use. The default is {@link HttpStatus#FOUND}. - *

      - * Note that according to RFC 7231, with {@link HttpStatus#FOUND}, a user agent MAY - * change the request method from POST to GET for the subsequent request. If this - * behavior is undesired, {@link HttpStatus#TEMPORARY_REDIRECT} can be used instead. - * @param statusCode the HTTP status code to use. - * @since 6.2 - */ - public void setStatusCode(HttpStatus statusCode) { - Assert.notNull(statusCode, "statusCode cannot be null"); - this.statusCode = statusCode; - } - } diff --git a/web/src/main/java/org/springframework/security/web/FilterInvocation.java b/web/src/main/java/org/springframework/security/web/FilterInvocation.java index f9f86476c8..a73f441674 100644 --- a/web/src/main/java/org/springframework/security/web/FilterInvocation.java +++ b/web/src/main/java/org/springframework/security/web/FilterInvocation.java @@ -146,7 +146,7 @@ public class FilterInvocation { @Override public String toString() { - if (!StringUtils.hasLength(this.request.getMethod())) { + if (StringUtils.isEmpty(this.request.getMethod())) { return "filter invocation [" + getRequestUrl() + "]"; } else { diff --git a/web/src/main/java/org/springframework/security/web/ObservationFilterChainDecorator.java b/web/src/main/java/org/springframework/security/web/ObservationFilterChainDecorator.java index ed24f19f91..f032af1a80 100644 --- a/web/src/main/java/org/springframework/security/web/ObservationFilterChainDecorator.java +++ b/web/src/main/java/org/springframework/security/web/ObservationFilterChainDecorator.java @@ -18,9 +18,7 @@ package org.springframework.security.web; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -141,49 +139,6 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F static final class ObservationFilter implements Filter { - static final Map OBSERVATION_NAMES = new HashMap<>(); - - static { - OBSERVATION_NAMES.put("DisableEncodeUrlFilter", "session.urlencoding"); - OBSERVATION_NAMES.put("ForceEagerSessionCreationFilter", "session.eagercreate"); - OBSERVATION_NAMES.put("ChannelProcessingFilter", "access.channel"); - OBSERVATION_NAMES.put("WebAsyncManagerIntegrationFilter", "context.async"); - OBSERVATION_NAMES.put("SecurityContextHolderFilter", "context.holder"); - OBSERVATION_NAMES.put("SecurityContextPersistenceFilter", "context.management"); - OBSERVATION_NAMES.put("HeaderWriterFilter", "header"); - OBSERVATION_NAMES.put("CorsFilter", "cors"); - OBSERVATION_NAMES.put("CsrfFilter", "csrf"); - OBSERVATION_NAMES.put("LogoutFilter", "logout"); - OBSERVATION_NAMES.put("OAuth2AuthorizationRequestRedirectFilter", "oauth2.authnrequest"); - OBSERVATION_NAMES.put("Saml2WebSsoAuthenticationRequestFilter", "saml2.authnrequest"); - OBSERVATION_NAMES.put("X509AuthenticationFilter", "authentication.x509"); - OBSERVATION_NAMES.put("J2eePreAuthenticatedProcessingFilter", "preauthentication.j2ee"); - OBSERVATION_NAMES.put("RequestHeaderAuthenticationFilter", "preauthentication.header"); - OBSERVATION_NAMES.put("RequestAttributeAuthenticationFilter", "preauthentication.attribute"); - OBSERVATION_NAMES.put("WebSpherePreAuthenticatedProcessingFilter", "preauthentication.websphere"); - OBSERVATION_NAMES.put("CasAuthenticationFilter", "cas.authentication"); - OBSERVATION_NAMES.put("OAuth2LoginAuthenticationFilter", "oauth2.authentication"); - OBSERVATION_NAMES.put("Saml2WebSsoAuthenticationFilter", "saml2.authentication"); - OBSERVATION_NAMES.put("UsernamePasswordAuthenticationFilter", "authentication.form"); - OBSERVATION_NAMES.put("DefaultLoginPageGeneratingFilter", "page.login"); - OBSERVATION_NAMES.put("DefaultLogoutPageGeneratingFilter", "page.logout"); - OBSERVATION_NAMES.put("ConcurrentSessionFilter", "session.concurrent"); - OBSERVATION_NAMES.put("DigestAuthenticationFilter", "authentication.digest"); - OBSERVATION_NAMES.put("BearerTokenAuthenticationFilter", "authentication.bearer"); - OBSERVATION_NAMES.put("BasicAuthenticationFilter", "authentication.basic"); - OBSERVATION_NAMES.put("RequestCacheAwareFilter", "requestcache"); - OBSERVATION_NAMES.put("SecurityContextHolderAwareRequestFilter", "context.servlet"); - OBSERVATION_NAMES.put("JaasApiIntegrationFilter", "jaas"); - OBSERVATION_NAMES.put("RememberMeAuthenticationFilter", "authentication.rememberme"); - OBSERVATION_NAMES.put("AnonymousAuthenticationFilter", "authentication.anonymous"); - OBSERVATION_NAMES.put("OAuth2AuthorizationCodeGrantFilter", "oauth2.client.code"); - OBSERVATION_NAMES.put("SessionManagementFilter", "session.management"); - OBSERVATION_NAMES.put("ExceptionTranslationFilter", "access.exceptions"); - OBSERVATION_NAMES.put("FilterSecurityInterceptor", "access.request"); - OBSERVATION_NAMES.put("AuthorizationFilter", "authorization"); - OBSERVATION_NAMES.put("SwitchUserFilter", "authentication.switch"); - } - private final ObservationRegistry registry; private final FilterChainObservationConvention convention = new FilterChainObservationConvention(); @@ -192,8 +147,6 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F private final String name; - private final String eventName; - private final int position; private final int size; @@ -204,12 +157,6 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F this.name = filter.getClass().getSimpleName(); this.position = position; this.size = size; - this.eventName = eventName(this.name); - } - - private String eventName(String className) { - String eventName = OBSERVATION_NAMES.get(className); - return (eventName != null) ? eventName : className; } String getName() { @@ -236,7 +183,7 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F parentBefore.setFilterName(this.name); parentBefore.setChainPosition(this.position); } - parent.before().event(Observation.Event.of(this.eventName + ".before", "before " + this.name)); + parent.before().event(Observation.Event.of(this.name + ".before", "before " + this.name)); this.filter.doFilter(request, response, chain); parent.start(); if (parent.after().getContext() instanceof FilterChainObservationContext parentAfter) { @@ -244,7 +191,7 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F parentAfter.setFilterName(this.name); parentAfter.setChainPosition(this.size - this.position + 1); } - parent.after().event(Observation.Event.of(this.eventName + ".after", "after " + this.name)); + parent.after().event(Observation.Event.of(this.name + ".after", "after " + this.name)); } private AroundFilterObservation parent(HttpServletRequest request) { diff --git a/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java b/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java index 9a38b1d231..8b669c2ef2 100644 --- a/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java +++ b/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java @@ -94,7 +94,7 @@ public class ExceptionTranslationFilter extends GenericFilterBean implements Mes private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); - private final RequestCache requestCache; + private RequestCache requestCache = new HttpSessionRequestCache(); protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); diff --git a/web/src/main/java/org/springframework/security/web/access/NoOpAccessDeniedHandler.java b/web/src/main/java/org/springframework/security/web/access/NoOpAccessDeniedHandler.java deleted file mode 100644 index 329a41ecf8..0000000000 --- a/web/src/main/java/org/springframework/security/web/access/NoOpAccessDeniedHandler.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2002-2023 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.web.access; - -import java.io.IOException; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import org.springframework.security.access.AccessDeniedException; - -/** - * An {@link AccessDeniedHandler} implementation that does nothing. - * - * @author Marcus da Coregio - * @since 6.2 - */ -public class NoOpAccessDeniedHandler implements AccessDeniedHandler { - - @Override - public void handle(HttpServletRequest request, HttpServletResponse response, - AccessDeniedException accessDeniedException) throws IOException, ServletException { - - } - -} diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java b/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java index 6c27365070..7419582881 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -170,25 +170,7 @@ public class AuthorizationFilter extends GenericFilterBean { * @param shouldFilterAllDispatcherTypes should filter all dispatcher types. Default * is {@code true} * @since 5.7 - * @deprecated Permit access to the {@link jakarta.servlet.DispatcherType} instead. - *

      -	 * @Configuration
      -	 * @EnableWebSecurity
      -	 * public class SecurityConfig {
      -	 *
      -	 * 	@Bean
      -	 * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      -	 * 		http
      -	 * 		 	.authorizeHttpRequests((authorize) -> authorize
      -	 * 				.dispatcherTypeMatchers(DispatcherType.ERROR).permitAll()
      -	 * 			 	// ...
      -	 * 		 	);
      -	 * 		return http.build();
      -	 * 	}
      -	 * }
      -	 * 
      */ - @Deprecated(since = "6.1", forRemoval = true) public void setShouldFilterAllDispatcherTypes(boolean shouldFilterAllDispatcherTypes) { this.observeOncePerRequest = !shouldFilterAllDispatcherTypes; this.filterErrorDispatch = shouldFilterAllDispatcherTypes; diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/RequestKey.java b/web/src/main/java/org/springframework/security/web/access/intercept/RequestKey.java index 43722a7a7f..b608e4f8b3 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/RequestKey.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/RequestKey.java @@ -48,9 +48,10 @@ public class RequestKey { @Override public boolean equals(Object obj) { - if (!(obj instanceof RequestKey key)) { + if (!(obj instanceof RequestKey)) { return false; } + RequestKey key = (RequestKey) obj; if (!this.url.equals(key.url)) { return false; } diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java index 7f00779c4e..6192426ac3 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,12 +26,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.log.LogMessage; -import org.springframework.security.authorization.AuthenticatedAuthorizationManager; -import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; -import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult; import org.springframework.security.web.util.matcher.RequestMatcherEntry; @@ -105,8 +102,6 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho */ public static final class Builder { - private boolean anyRequestConfigured; - private final List>> mappings = new ArrayList<>(); /** @@ -116,7 +111,6 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho * @return the {@link Builder} for further customizations */ public Builder add(RequestMatcher matcher, AuthorizationManager manager) { - Assert.state(!this.anyRequestConfigured, "Can't add mappings after anyRequest"); Assert.notNull(matcher, "matcher cannot be null"); Assert.notNull(manager, "manager cannot be null"); this.mappings.add(new RequestMatcherEntry<>(matcher, manager)); @@ -133,34 +127,11 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho */ public Builder mappings( Consumer>>> mappingsConsumer) { - Assert.state(!this.anyRequestConfigured, "Can't configure mappings after anyRequest"); Assert.notNull(mappingsConsumer, "mappingsConsumer cannot be null"); mappingsConsumer.accept(this.mappings); return this; } - /** - * Maps any request. - * @return the {@link AuthorizedUrl} for further customizations - * @since 6.2 - */ - public AuthorizedUrl anyRequest() { - Assert.state(!this.anyRequestConfigured, "Can't configure anyRequest after itself"); - this.anyRequestConfigured = true; - return new AuthorizedUrl(AnyRequestMatcher.INSTANCE); - } - - /** - * Maps {@link RequestMatcher}s to {@link AuthorizationManager}. - * @param matchers the {@link RequestMatcher}s to map - * @return the {@link AuthorizedUrl} for further customizations - * @since 6.2 - */ - public AuthorizedUrl requestMatchers(RequestMatcher... matchers) { - Assert.state(!this.anyRequestConfigured, "Can't configure requestMatchers after anyRequest"); - return new AuthorizedUrl(matchers); - } - /** * Creates a {@link RequestMatcherDelegatingAuthorizationManager} instance. * @return the {@link RequestMatcherDelegatingAuthorizationManager} instance @@ -169,123 +140,6 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho return new RequestMatcherDelegatingAuthorizationManager(this.mappings); } - /** - * An object that allows configuring the {@link AuthorizationManager} for - * {@link RequestMatcher}s. - * - * @author Evgeniy Cheban - * @since 6.2 - */ - public final class AuthorizedUrl { - - private final List matchers; - - private AuthorizedUrl(RequestMatcher... matchers) { - this(List.of(matchers)); - } - - private AuthorizedUrl(List matchers) { - this.matchers = matchers; - } - - /** - * Specify that URLs are allowed by anyone. - * @return the {@link Builder} for further customizations - */ - public Builder permitAll() { - return access((a, o) -> new AuthorizationDecision(true)); - } - - /** - * Specify that URLs are not allowed by anyone. - * @return the {@link Builder} for further customizations - */ - public Builder denyAll() { - return access((a, o) -> new AuthorizationDecision(false)); - } - - /** - * Specify that URLs are allowed by any authenticated user. - * @return the {@link Builder} for further customizations - */ - public Builder authenticated() { - return access(AuthenticatedAuthorizationManager.authenticated()); - } - - /** - * Specify that URLs are allowed by users who have authenticated and were not - * "remembered". - * @return the {@link Builder} for further customization - */ - public Builder fullyAuthenticated() { - return access(AuthenticatedAuthorizationManager.fullyAuthenticated()); - } - - /** - * Specify that URLs are allowed by users that have been remembered. - * @return the {@link Builder} for further customization - */ - public Builder rememberMe() { - return access(AuthenticatedAuthorizationManager.rememberMe()); - } - - /** - * Specify that URLs are allowed by anonymous users. - * @return the {@link Builder} for further customization - */ - public Builder anonymous() { - return access(AuthenticatedAuthorizationManager.anonymous()); - } - - /** - * Specifies a user requires a role. - * @param role the role that should be required which is prepended with ROLE_ - * automatically (i.e. USER, ADMIN, etc). It should not start with ROLE_ - * @return {@link Builder} for further customizations - */ - public Builder hasRole(String role) { - return access(AuthorityAuthorizationManager.hasRole(role)); - } - - /** - * Specifies that a user requires one of many roles. - * @param roles the roles that the user should have at least one of (i.e. - * ADMIN, USER, etc). Each role should not start with ROLE_ since it is - * automatically prepended already - * @return the {@link Builder} for further customizations - */ - public Builder hasAnyRole(String... roles) { - return access(AuthorityAuthorizationManager.hasAnyRole(roles)); - } - - /** - * Specifies a user requires an authority. - * @param authority the authority that should be required - * @return the {@link Builder} for further customizations - */ - public Builder hasAuthority(String authority) { - return access(AuthorityAuthorizationManager.hasAuthority(authority)); - } - - /** - * Specifies that a user requires one of many authorities. - * @param authorities the authorities that the user should have at least one - * of (i.e. ROLE_USER, ROLE_ADMIN, etc) - * @return the {@link Builder} for further customizations - */ - public Builder hasAnyAuthority(String... authorities) { - return access(AuthorityAuthorizationManager.hasAnyAuthority(authorities)); - } - - private Builder access(AuthorizationManager manager) { - for (RequestMatcher matcher : this.matchers) { - Builder.this.mappings.add(new RequestMatcherEntry<>(matcher, manager)); - } - return Builder.this; - } - - } - } } diff --git a/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationTargetUrlRequestHandler.java b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationTargetUrlRequestHandler.java index 55e8a79603..e33c41309b 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationTargetUrlRequestHandler.java +++ b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationTargetUrlRequestHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 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. @@ -107,36 +107,31 @@ public abstract class AbstractAuthenticationTargetUrlRequestHandler { if (isAlwaysUseDefaultTargetUrl()) { return this.defaultTargetUrl; } - String targetUrlParameterValue = getTargetUrlParameterValue(request); - if (StringUtils.hasText(targetUrlParameterValue)) { - trace("Using url %s from request parameter %s", targetUrlParameterValue, this.targetUrlParameter); - return targetUrlParameterValue; + // Check for the parameter and use that if available + String targetUrl = null; + if (this.targetUrlParameter != null) { + targetUrl = request.getParameter(this.targetUrlParameter); + if (StringUtils.hasText(targetUrl)) { + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Using url %s from request parameter %s", targetUrl, + this.targetUrlParameter)); + } + return targetUrl; + } } - if (this.useReferer) { - trace("Using url %s from Referer header", request.getHeader("Referer")); - return request.getHeader("Referer"); + if (this.useReferer && !StringUtils.hasLength(targetUrl)) { + targetUrl = request.getHeader("Referer"); + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Using url %s from Referer header", targetUrl)); + } } - return this.defaultTargetUrl; - } - - private String getTargetUrlParameterValue(HttpServletRequest request) { - if (this.targetUrlParameter == null) { - return null; - } - String value = request.getParameter(this.targetUrlParameter); - if (value == null) { - return null; - } - if (StringUtils.hasText(value)) { - return value; - } - return this.defaultTargetUrl; - } - - private void trace(String msg, String... msgParts) { - if (this.logger.isTraceEnabled()) { - this.logger.trace(LogMessage.format(msg, msgParts)); + if (!StringUtils.hasText(targetUrl)) { + targetUrl = this.defaultTargetUrl; + if (this.logger.isTraceEnabled()) { + this.logger.trace(LogMessage.format("Using default url %s", targetUrl)); + } } + return targetUrl; } /** diff --git a/web/src/main/java/org/springframework/security/web/authentication/NoOpAuthenticationEntryPoint.java b/web/src/main/java/org/springframework/security/web/authentication/NoOpAuthenticationEntryPoint.java deleted file mode 100644 index 30da537610..0000000000 --- a/web/src/main/java/org/springframework/security/web/authentication/NoOpAuthenticationEntryPoint.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2002-2023 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.web.authentication; - -import java.io.IOException; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; - -/** - * An {@link AuthenticationEntryPoint} implementation that does nothing. - * - * @author Marcus da Coregio - * @since 6.2 - */ -public class NoOpAuthenticationEntryPoint implements AuthenticationEntryPoint { - - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException, ServletException { - - } - -} diff --git a/web/src/main/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServices.java b/web/src/main/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServices.java index 7e1369fa76..02309f04af 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServices.java +++ b/web/src/main/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServices.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 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. @@ -119,7 +119,7 @@ public abstract class AbstractRememberMeServices * which in turn is used to create a valid authentication token. */ @Override - public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { + public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { String rememberMeCookie = extractRememberMeCookie(request); if (rememberMeCookie == null) { return null; @@ -253,7 +253,7 @@ public abstract class AbstractRememberMeServices } @Override - public void loginFail(HttpServletRequest request, HttpServletResponse response) { + public final void loginFail(HttpServletRequest request, HttpServletResponse response) { this.logger.debug("Interactive login attempt was unsuccessful."); cancelCookie(request, response); onLoginFail(request, response); @@ -268,11 +268,11 @@ public abstract class AbstractRememberMeServices *

      * Examines the incoming request and checks for the presence of the configured * "remember me" parameter. If it's present, or if alwaysRemember is set to - * true, calls onLoginSuccess. + * true, calls onLoginSucces. *

      */ @Override - public void loginSuccess(HttpServletRequest request, HttpServletResponse response, + public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { if (!rememberMeRequested(request, this.parameter)) { this.logger.debug("Remember-me login not requested."); diff --git a/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java index 164c1fc8c6..b954b4138c 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/rememberme/RememberMeAuthenticationFilter.java @@ -112,7 +112,7 @@ public class RememberMeAuthenticationFilter extends GenericFilterBean implements } Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response); if (rememberMeAuth != null) { - // Attempt authentication via AuthenticationManager + // Attempt authenticaton via AuthenticationManager try { rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth); // Store to SecurityContextHolder diff --git a/web/src/main/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.java b/web/src/main/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.java index f9c72df806..46eada5192 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.java +++ b/web/src/main/java/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.java @@ -167,7 +167,7 @@ public class TokenBasedRememberMeServices extends AbstractRememberMeServices { private long getTokenExpiryTime(String[] cookieTokens) { try { - return Long.valueOf(cookieTokens[1]); + return new Long(cookieTokens[1]); } catch (NumberFormatException nfe) { throw new InvalidCookieException( diff --git a/web/src/main/java/org/springframework/security/web/authentication/switchuser/SwitchUserGrantedAuthority.java b/web/src/main/java/org/springframework/security/web/authentication/switchuser/SwitchUserGrantedAuthority.java index 4be683e554..a05155e9e0 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/switchuser/SwitchUserGrantedAuthority.java +++ b/web/src/main/java/org/springframework/security/web/authentication/switchuser/SwitchUserGrantedAuthority.java @@ -64,8 +64,9 @@ public final class SwitchUserGrantedAuthority implements GrantedAuthority { if (this == obj) { return true; } - if (obj instanceof SwitchUserGrantedAuthority swa) { - return this.role.equals(swa.getAuthority()) && this.source.equals(swa.getSource()); + if (obj instanceof SwitchUserGrantedAuthority) { + SwitchUserGrantedAuthority swa = (SwitchUserGrantedAuthority) obj; + return this.role.equals(swa.role) && this.source.equals(swa.source); } return false; } diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java index b70649a8e3..6cfdb6faae 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -96,8 +96,8 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { this.formLoginEnabled = true; this.usernameParameter = authFilter.getUsernameParameter(); this.passwordParameter = authFilter.getPasswordParameter(); - if (authFilter.getRememberMeServices() instanceof AbstractRememberMeServices rememberMeServices) { - this.rememberMeParameter = rememberMeServices.getParameter(); + if (authFilter.getRememberMeServices() instanceof AbstractRememberMeServices) { + this.rememberMeParameter = ((AbstractRememberMeServices) authFilter.getRememberMeServices()).getParameter(); } } @@ -189,7 +189,15 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { } private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) { - String errorMsg = loginError ? getLoginErrorMessage(request) : "Invalid credentials"; + String errorMsg = "Invalid credentials"; + if (loginError) { + HttpSession session = request.getSession(false); + if (session != null) { + AuthenticationException ex = (AuthenticationException) session + .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); + errorMsg = (ex != null) ? ex.getMessage() : "Invalid credentials"; + } + } String contextPath = request.getContextPath(); StringBuilder sb = new StringBuilder(); sb.append("\n"); @@ -203,7 +211,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { sb.append(" \n"); sb.append(" \n"); + + "rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"); sb.append(" \n"); sb.append(" \n"); sb.append("
      \n"); @@ -264,15 +272,6 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { return sb.toString(); } - private String getLoginErrorMessage(HttpServletRequest request) { - HttpSession session = request.getSession(false); - if (session != null && session - .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION) instanceof AuthenticationException exception) { - return exception.getMessage(); - } - return "Invalid credentials"; - } - private String renderHiddenInputs(HttpServletRequest request) { StringBuilder sb = new StringBuilder(); for (Map.Entry input : this.resolveHiddenInputs.apply(request).entrySet()) { @@ -304,14 +303,14 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { return matches(request, this.failureUrl); } - private String createError(boolean isError, String message) { + private static String createError(boolean isError, String message) { if (!isError) { return ""; } return "
      " + HtmlUtils.htmlEscape(message) + "
      "; } - private String createLogoutSuccess(boolean isLogoutSuccess) { + private static String createLogoutSuccess(boolean isLogoutSuccess) { if (!isLogoutSuccess) { return ""; } diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java index 29f4b3d5d5..40a1ab84fc 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -73,7 +73,7 @@ public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter { + "rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" " + "crossorigin=\"anonymous\">\n"); sb.append(" \n"); + + "rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"); sb.append(" \n"); sb.append(" \n"); sb.append("
      \n"); diff --git a/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java index 8841937c51..c91099e4bf 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java @@ -28,16 +28,15 @@ import org.springframework.core.log.LogMessage; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.NullRememberMeServices; import org.springframework.security.web.authentication.RememberMeServices; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.util.Assert; @@ -106,7 +105,7 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { private String credentialsCharset = "UTF-8"; - private AuthenticationConverter authenticationConverter = new BasicAuthenticationConverter(); + private BasicAuthenticationConverter authenticationConverter = new BasicAuthenticationConverter(); private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository(); @@ -150,18 +149,6 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { this.securityContextRepository = securityContextRepository; } - /** - * Sets the - * {@link org.springframework.security.web.authentication.AuthenticationConverter} to - * use. Defaults to {@link BasicAuthenticationConverter} - * @param authenticationConverter the converter to use - * @since 6.2 - */ - public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) { - Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); - this.authenticationConverter = authenticationConverter; - } - @Override public void afterPropertiesSet() { Assert.notNull(this.authenticationManager, "An AuthenticationManager is required"); @@ -174,7 +161,7 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { try { - Authentication authRequest = this.authenticationConverter.convert(request); + UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request); if (authRequest == null) { this.logger.trace("Did not process authentication request since failed to find " + "username and password in Basic Authorization header"); @@ -263,19 +250,9 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { this.securityContextHolderStrategy = securityContextHolderStrategy; } - /** - * Sets the {@link AuthenticationDetailsSource} to use. By default, it is set to use - * the {@link WebAuthenticationDetailsSource}. Note that this configuration applies - * exclusively when the {@link #authenticationConverter} is set to - * {@link BasicAuthenticationConverter}. If you are utilizing a different - * implementation, you will need to manually specify the authentication details on it. - * @param authenticationDetailsSource the {@link AuthenticationDetailsSource} to use. - */ public void setAuthenticationDetailsSource( AuthenticationDetailsSource authenticationDetailsSource) { - if (this.authenticationConverter instanceof BasicAuthenticationConverter basicAuthenticationConverter) { - basicAuthenticationConverter.setAuthenticationDetailsSource(authenticationDetailsSource); - } + this.authenticationConverter.setAuthenticationDetailsSource(authenticationDetailsSource); } public void setRememberMeServices(RememberMeServices rememberMeServices) { @@ -283,20 +260,10 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { this.rememberMeServices = rememberMeServices; } - /** - * Sets the charset to use when decoding credentials to {@link String}s. By default, - * it is set to {@code UTF-8}. Note that this configuration applies exclusively when - * the {@link #authenticationConverter} is set to - * {@link BasicAuthenticationConverter}. If you are utilizing a different - * implementation, you will need to manually specify the charset on it. - * @param credentialsCharset the charset to use. - */ public void setCredentialsCharset(String credentialsCharset) { Assert.hasText(credentialsCharset, "credentialsCharset cannot be null or empty"); this.credentialsCharset = credentialsCharset; - if (this.authenticationConverter instanceof BasicAuthenticationConverter basicAuthenticationConverter) { - basicAuthenticationConverter.setCredentialsCharset(Charset.forName(credentialsCharset)); - } + this.authenticationConverter.setCredentialsCharset(Charset.forName(credentialsCharset)); } protected String getCredentialsCharset(HttpServletRequest httpRequest) { diff --git a/web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java index bf23561280..49242172f9 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java @@ -385,7 +385,7 @@ public class DigestAuthenticationFilter extends GenericFilterBean implements Mes } // Extract expiry time from nonce try { - this.nonceExpiryTime = Long.valueOf(nonceTokens[0]); + this.nonceExpiryTime = new Long(nonceTokens[0]); } catch (NumberFormatException nfe) { throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage( diff --git a/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java b/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java index 86811c7fd4..3d8893f927 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java +++ b/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,12 @@ package org.springframework.security.web.csrf; import java.util.UUID; -import java.util.function.Consumer; +import jakarta.servlet.ServletRequest; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.util.WebUtils; @@ -36,7 +34,6 @@ import org.springframework.web.util.WebUtils; * * @author Rob Winch * @author Steve Riesenberg - * @author Alex Montoya * @since 4.1 */ public final class CookieCsrfTokenRepository implements CsrfTokenRepository { @@ -66,18 +63,7 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository { private int cookieMaxAge = -1; - private Consumer cookieCustomizer = (builder) -> { - }; - - /** - * Add a {@link Consumer} for a {@code ResponseCookieBuilder} that will be invoked for - * each cookie being built, just before the call to {@code build()}. - * @param cookieCustomizer consumer for a cookie builder - * @since 6.1 - */ - public void setCookieCustomizer(Consumer cookieCustomizer) { - Assert.notNull(cookieCustomizer, "cookieCustomizer must not be null"); - this.cookieCustomizer = cookieCustomizer; + public CookieCsrfTokenRepository() { } @Override @@ -88,17 +74,15 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository { @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { String tokenValue = (token != null) ? token.getToken() : ""; - - ResponseCookie.ResponseCookieBuilder cookieBuilder = ResponseCookie.from(this.cookieName, tokenValue) - .secure((this.secure != null) ? this.secure : request.isSecure()) - .path(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request)) - .maxAge((token != null) ? this.cookieMaxAge : 0) - .httpOnly(this.cookieHttpOnly) - .domain(this.cookieDomain); - - this.cookieCustomizer.accept(cookieBuilder); - - response.addHeader(HttpHeaders.SET_COOKIE, cookieBuilder.build().toString()); + Cookie cookie = new Cookie(this.cookieName, tokenValue); + cookie.setSecure((this.secure != null) ? this.secure : request.isSecure()); + cookie.setPath(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request)); + cookie.setMaxAge((token != null) ? this.cookieMaxAge : 0); + cookie.setHttpOnly(this.cookieHttpOnly); + if (StringUtils.hasLength(this.cookieDomain)) { + cookie.setDomain(this.cookieDomain); + } + response.addCookie(cookie); // Set request attribute to signal that response has blank cookie value, // which allows loadToken to return null when token has been removed @@ -159,9 +143,11 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository { } /** - * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead. + * Sets the HttpOnly attribute on the cookie containing the CSRF token. Defaults to + * true. + * @param cookieHttpOnly true sets the HttpOnly attribute, + * false does not set it */ - @Deprecated(since = "6.1") public void setCookieHttpOnly(boolean cookieHttpOnly) { this.cookieHttpOnly = cookieHttpOnly; } @@ -172,14 +158,14 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository { } /** - * Factory method to conveniently create an instance that creates cookies where - * {@link Cookie#isHttpOnly()} is set to false. - * @return an instance of CookieCsrfTokenRepository that creates cookies where - * {@link Cookie#isHttpOnly()} is set to false. + * Factory method to conveniently create an instance that has + * {@link #setCookieHttpOnly(boolean)} set to false. + * @return an instance of CookieCsrfTokenRepository with + * {@link #setCookieHttpOnly(boolean)} set to false */ public static CookieCsrfTokenRepository withHttpOnlyFalse() { CookieCsrfTokenRepository result = new CookieCsrfTokenRepository(); - result.cookieHttpOnly = false; + result.setCookieHttpOnly(false); return result; } @@ -205,28 +191,48 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository { } /** + * Sets the domain of the cookie that the expected CSRF token is saved to and read + * from. + * @param cookieDomain the domain of the cookie that the expected CSRF token is saved + * to and read from * @since 5.2 - * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead. */ - @Deprecated(since = "6.1") public void setCookieDomain(String cookieDomain) { this.cookieDomain = cookieDomain; } /** + * Sets secure flag of the cookie that the expected CSRF token is saved to and read + * from. By default secure flag depends on {@link ServletRequest#isSecure()} + * @param secure the secure flag of the cookie that the expected CSRF token is saved + * to and read from * @since 5.4 - * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead. */ - @Deprecated(since = "6.1") public void setSecure(Boolean secure) { this.secure = secure; } /** + * Sets maximum age in seconds for the cookie that the expected CSRF token is saved to + * and read from. By default maximum age value is -1. + * + *

      + * A positive value indicates that the cookie will expire after that many seconds have + * passed. Note that the value is the maximum age when the cookie will expire, + * not the cookie's current age. + * + *

      + * A negative value means that the cookie is not stored persistently and will be + * deleted when the Web browser exits. + * + *

      + * A zero value causes the cookie to be deleted immediately therefore it is not a + * valid value and in that case an {@link IllegalArgumentException} will be thrown. + * @param cookieMaxAge an integer specifying the maximum age of the cookie in seconds; + * if negative, means the cookie is not stored; if zero, the method throws an + * {@link IllegalArgumentException} * @since 5.5 - * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead. */ - @Deprecated(since = "6.1") public void setCookieMaxAge(int cookieMaxAge) { Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero"); this.cookieMaxAge = cookieMaxAge; diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java index e6034b6bfb..988f094ea6 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java +++ b/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java @@ -51,9 +51,9 @@ import org.springframework.web.filter.OncePerRequestFilter; * *

      * Typically the {@link CsrfTokenRepository} implementation chooses to store the - * {@link CsrfToken} in {@link HttpSession} with {@link HttpSessionCsrfTokenRepository}. - * This is preferred to storing the token in a cookie which can be modified by a client - * application. + * {@link CsrfToken} in {@link HttpSession} with {@link HttpSessionCsrfTokenRepository} + * wrapped by a {@link LazyCsrfTokenRepository}. This is preferred to storing the token in + * a cookie which can be modified by a client application. *

      * * @author Rob Winch @@ -72,7 +72,7 @@ public final class CsrfFilter extends OncePerRequestFilter { /** * The attribute name to use when marking a given request as one that should not be * filtered. - *

      + * * To use, set the attribute on your {@link HttpServletRequest}:

       	 * 	CsrfFilter.skipRequest(request);
       	 * 
      diff --git a/web/src/main/java/org/springframework/security/web/csrf/XorCsrfTokenRequestAttributeHandler.java b/web/src/main/java/org/springframework/security/web/csrf/XorCsrfTokenRequestAttributeHandler.java index 8d966331ae..0a1a42d3a3 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/XorCsrfTokenRequestAttributeHandler.java +++ b/web/src/main/java/org/springframework/security/web/csrf/XorCsrfTokenRequestAttributeHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,7 +97,7 @@ public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestA System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize); byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf); - return (csrfBytes != null) ? Utf8.decode(csrfBytes) : null; + return Utf8.decode(csrfBytes); } private static String createXoredCsrfToken(SecureRandom secureRandom, String token) { @@ -114,9 +114,6 @@ public final class XorCsrfTokenRequestAttributeHandler extends CsrfTokenRequestA } private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) { - if (csrfBytes.length < randomBytes.length) { - return null; - } int len = Math.min(randomBytes.length, csrfBytes.length); byte[] xoredCsrf = new byte[len]; System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length); diff --git a/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java b/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java index 585ecd503f..9f3cd7ee33 100644 --- a/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java +++ b/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -713,7 +713,7 @@ public class StrictHttpFirewall implements HttpFirewall { } String value = super.getHeader(name); if (value != null) { - validateAllowedHeaderValue(name, value); + validateAllowedHeaderValue(value); } return value; } @@ -734,7 +734,7 @@ public class StrictHttpFirewall implements HttpFirewall { @Override public String nextElement() { String value = headers.nextElement(); - validateAllowedHeaderValue(name, value); + validateAllowedHeaderValue(value); return value; } @@ -768,7 +768,7 @@ public class StrictHttpFirewall implements HttpFirewall { } String value = super.getParameter(name); if (value != null) { - validateAllowedParameterValue(name, value); + validateAllowedParameterValue(value); } return value; } @@ -781,7 +781,7 @@ public class StrictHttpFirewall implements HttpFirewall { String[] values = entry.getValue(); validateAllowedParameterName(name); for (String value : values) { - validateAllowedParameterValue(name, value); + validateAllowedParameterValue(value); } } return parameterMap; @@ -815,7 +815,7 @@ public class StrictHttpFirewall implements HttpFirewall { String[] values = super.getParameterValues(name); if (values != null) { for (String value : values) { - validateAllowedParameterValue(name, value); + validateAllowedParameterValue(value); } } return values; @@ -828,10 +828,10 @@ public class StrictHttpFirewall implements HttpFirewall { } } - private void validateAllowedHeaderValue(String name, String value) { + private void validateAllowedHeaderValue(String value) { if (!StrictHttpFirewall.this.allowedHeaderValues.test(value)) { - throw new RequestRejectedException("The request was rejected because the header: \"" + name - + " \" has a value \"" + value + "\" that is not allowed."); + throw new RequestRejectedException( + "The request was rejected because the header value \"" + value + "\" is not allowed."); } } @@ -842,10 +842,10 @@ public class StrictHttpFirewall implements HttpFirewall { } } - private void validateAllowedParameterValue(String name, String value) { + private void validateAllowedParameterValue(String value) { if (!StrictHttpFirewall.this.allowedParameterValues.test(value)) { - throw new RequestRejectedException("The request was rejected because the parameter: \"" + name - + " \" has a value \"" + value + "\" that is not allowed."); + throw new RequestRejectedException( + "The request was rejected because the parameter value \"" + value + "\" is not allowed."); } } diff --git a/web/src/main/java/org/springframework/security/web/jaasapi/JaasApiIntegrationFilter.java b/web/src/main/java/org/springframework/security/web/jaasapi/JaasApiIntegrationFilter.java index bc590e37a5..4bdb78cedf 100644 --- a/web/src/main/java/org/springframework/security/web/jaasapi/JaasApiIntegrationFilter.java +++ b/web/src/main/java/org/springframework/security/web/jaasapi/JaasApiIntegrationFilter.java @@ -127,9 +127,10 @@ public class JaasApiIntegrationFilter extends GenericFilterBean { if (!authentication.isAuthenticated()) { return null; } - if (!(authentication instanceof JaasAuthenticationToken token)) { + if (!(authentication instanceof JaasAuthenticationToken)) { return null; } + JaasAuthenticationToken token = (JaasAuthenticationToken) authentication; LoginContext loginContext = token.getLoginContext(); if (loginContext == null) { return null; diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/CookieRequestCache.java b/web/src/main/java/org/springframework/security/web/savedrequest/CookieRequestCache.java index 8f245eb7ea..bd1719b151 100644 --- a/web/src/main/java/org/springframework/security/web/savedrequest/CookieRequestCache.java +++ b/web/src/main/java/org/springframework/security/web/savedrequest/CookieRequestCache.java @@ -128,7 +128,7 @@ public class CookieRequestCache implements RequestCache { private static String getCookiePath(HttpServletRequest request) { String contextPath = request.getContextPath(); - return (StringUtils.hasLength(contextPath)) ? contextPath : "/"; + return (!StringUtils.isEmpty(contextPath)) ? contextPath : "/"; } private boolean matchesSavedRequest(HttpServletRequest request, SavedRequest savedRequest) { diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/SavedCookie.java b/web/src/main/java/org/springframework/security/web/savedrequest/SavedCookie.java index c4d6dbce33..c9455882af 100644 --- a/web/src/main/java/org/springframework/security/web/savedrequest/SavedCookie.java +++ b/web/src/main/java/org/springframework/security/web/savedrequest/SavedCookie.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,28 +31,22 @@ public class SavedCookie implements Serializable { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; - private final String name; + private final java.lang.String name; - private final String value; + private final java.lang.String value; - private final String comment; + private final java.lang.String comment; - private final String domain; + private final java.lang.String domain; private final int maxAge; - private final String path; + private final java.lang.String path; private final boolean secure; private final int version; - /** - * @deprecated use - * {@link org.springframework.security.web.savedrequest.SavedCookie#SavedCookie(String, String, String, int, String, boolean)} - * instead - */ - @Deprecated(forRemoval = true, since = "6.1") public SavedCookie(String name, String value, String comment, String domain, int maxAge, String path, boolean secure, int version) { this.name = name; @@ -65,13 +59,9 @@ public class SavedCookie implements Serializable { this.version = version; } - public SavedCookie(String name, String value, String domain, int maxAge, String path, boolean secure) { - this(name, value, null, domain, maxAge, path, secure, 0); - } - public SavedCookie(Cookie cookie) { - this(cookie.getName(), cookie.getValue(), cookie.getDomain(), cookie.getMaxAge(), cookie.getPath(), - cookie.getSecure()); + this(cookie.getName(), cookie.getValue(), cookie.getComment(), cookie.getDomain(), cookie.getMaxAge(), + cookie.getPath(), cookie.getSecure(), cookie.getVersion()); } public String getName() { @@ -82,7 +72,6 @@ public class SavedCookie implements Serializable { return this.value; } - @Deprecated(forRemoval = true, since = "6.1") public String getComment() { return this.comment; } @@ -103,7 +92,6 @@ public class SavedCookie implements Serializable { return this.secure; } - @Deprecated(forRemoval = true, since = "6.1") public int getVersion() { return this.version; } diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/SavedRequestAwareWrapper.java b/web/src/main/java/org/springframework/security/web/savedrequest/SavedRequestAwareWrapper.java index 77c5207cd8..200578d595 100644 --- a/web/src/main/java/org/springframework/security/web/savedrequest/SavedRequestAwareWrapper.java +++ b/web/src/main/java/org/springframework/security/web/savedrequest/SavedRequestAwareWrapper.java @@ -33,8 +33,6 @@ import jakarta.servlet.http.HttpServletRequestWrapper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.http.HttpHeaders; - /** * Provides request parameters, headers and cookies from either an original request or a * saved request. @@ -59,7 +57,10 @@ class SavedRequestAwareWrapper extends HttpServletRequestWrapper { protected static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT"); - protected SavedRequest savedRequest; + /** The default Locale if none are specified. */ + protected static Locale defaultLocale = Locale.getDefault(); + + protected SavedRequest savedRequest = null; /** * The set of SimpleDateFormat formats to use in getDateHeader(). Notice that because @@ -100,12 +101,14 @@ class SavedRequestAwareWrapper extends HttpServletRequestWrapper { } @Override - public Enumeration getHeaderNames() { + @SuppressWarnings("unchecked") + public Enumeration getHeaderNames() { return new Enumerator<>(this.savedRequest.getHeaderNames()); } @Override - public Enumeration getHeaders(String name) { + @SuppressWarnings("unchecked") + public Enumeration getHeaders(String name) { return new Enumerator<>(this.savedRequest.getHeaderValues(name)); } @@ -122,7 +125,8 @@ class SavedRequestAwareWrapper extends HttpServletRequestWrapper { } @Override - public Enumeration getLocales() { + @SuppressWarnings("unchecked") + public Enumeration getLocales() { List locales = this.savedRequest.getLocales(); if (locales.isEmpty()) { // Fall back to default locale @@ -137,11 +141,6 @@ class SavedRequestAwareWrapper extends HttpServletRequestWrapper { return this.savedRequest.getMethod(); } - @Override - public String getContentType() { - return getHeader(HttpHeaders.CONTENT_TYPE); - } - /** * If the parameter is available from the wrapped request then the request has been * forwarded/included to a URL with parameters, either supplementing or overriding the @@ -166,7 +165,8 @@ class SavedRequestAwareWrapper extends HttpServletRequestWrapper { } @Override - public Map getParameterMap() { + @SuppressWarnings("unchecked") + public Map getParameterMap() { Set names = getCombinedParameterNames(); Map parameterMap = new HashMap<>(names.size()); for (String name : names) { @@ -175,6 +175,7 @@ class SavedRequestAwareWrapper extends HttpServletRequestWrapper { return parameterMap; } + @SuppressWarnings("unchecked") private Set getCombinedParameterNames() { Set names = new HashSet<>(); names.addAll(super.getParameterMap().keySet()); @@ -183,8 +184,9 @@ class SavedRequestAwareWrapper extends HttpServletRequestWrapper { } @Override - public Enumeration getParameterNames() { - return new Enumerator<>(getCombinedParameterNames()); + @SuppressWarnings("unchecked") + public Enumeration getParameterNames() { + return new Enumerator(getCombinedParameterNames()); } @Override diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/SimpleSavedRequest.java b/web/src/main/java/org/springframework/security/web/savedrequest/SimpleSavedRequest.java index 08165eb0cd..e1595ea6bd 100644 --- a/web/src/main/java/org/springframework/security/web/savedrequest/SimpleSavedRequest.java +++ b/web/src/main/java/org/springframework/security/web/savedrequest/SimpleSavedRequest.java @@ -126,7 +126,7 @@ public class SimpleSavedRequest implements SavedRequest { } public void setLocales(List locales) { - Assert.notNull(locales, "locales cannot be null"); + Assert.notNull("locales cannot be null"); this.locales = locales; } diff --git a/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java index a851af1df2..01fa28c6b0 100644 --- a/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java +++ b/web/src/main/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverter.java @@ -46,7 +46,7 @@ public class ServerFormLoginAuthenticationConverter implements Function apply(ServerWebExchange exchange) { - return exchange.getFormData().map(this::createAuthentication); + return exchange.getFormData().map((data) -> createAuthentication(data)); } private UsernamePasswordAuthenticationToken createAuthentication(MultiValueMap data) { diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/SwitchUserWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authentication/SwitchUserWebFilter.java index cfc93a301a..88e8511af6 100644 --- a/web/src/main/java/org/springframework/security/web/server/authentication/SwitchUserWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/authentication/SwitchUserWebFilter.java @@ -280,7 +280,8 @@ public class SwitchUserWebFilter implements WebFilter { private Optional extractSourceAuthentication(Authentication currentAuthentication) { // iterate over granted authorities and find the 'switch user' authority for (GrantedAuthority authority : currentAuthentication.getAuthorities()) { - if (authority instanceof SwitchUserGrantedAuthority switchAuthority) { + if (authority instanceof SwitchUserGrantedAuthority) { + SwitchUserGrantedAuthority switchAuthority = (SwitchUserGrantedAuthority) authority; return Optional.of(switchAuthority.getSource()); } } diff --git a/web/src/main/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepository.java b/web/src/main/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepository.java index 40301e5de7..68fac2fdad 100644 --- a/web/src/main/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepository.java +++ b/web/src/main/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepository.java @@ -17,7 +17,6 @@ package org.springframework.security.web.server.csrf; import java.util.UUID; -import java.util.function.Consumer; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @@ -37,7 +36,6 @@ import org.springframework.web.server.ServerWebExchange; * @author Eric Deandrea * @author Thomas Vitale * @author Alonso Araya - * @author Alex Montoya * @since 5.1 */ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRepository { @@ -62,29 +60,15 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep private int cookieMaxAge = -1; - private Consumer cookieCustomizer = (builder) -> { - }; - /** - * Add a {@link Consumer} for a {@code ResponseCookieBuilder} that will be invoked for - * each cookie being built, just before the call to {@code build()}. - * @param cookieCustomizer consumer for a cookie builder - * @since 6.1 - */ - public void setCookieCustomizer(Consumer cookieCustomizer) { - Assert.notNull(cookieCustomizer, "cookieCustomizer must not be null"); - this.cookieCustomizer = cookieCustomizer; - } - - /** - * Factory method to conveniently create an instance that has creates cookies with - * {@link ResponseCookie#isHttpOnly} set to false. - * @return an instance of CookieCsrfTokenRepository that creates cookies with - * {@link ResponseCookie#isHttpOnly} set to false + * Factory method to conveniently create an instance that has + * {@link #setCookieHttpOnly(boolean)} set to false. + * @return an instance of CookieCsrfTokenRepository with + * {@link #setCookieHttpOnly(boolean)} set to false */ public static CookieServerCsrfTokenRepository withHttpOnlyFalse() { CookieServerCsrfTokenRepository result = new CookieServerCsrfTokenRepository(); - result.setCookieCustomizer((cookie) -> cookie.httpOnly(false)); + result.setCookieHttpOnly(false); return result; } @@ -98,18 +82,16 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep return Mono.fromRunnable(() -> { String tokenValue = (token != null) ? token.getToken() : ""; // @formatter:off - ResponseCookie.ResponseCookieBuilder cookieBuilder = ResponseCookie + ResponseCookie cookie = ResponseCookie .from(this.cookieName, tokenValue) .domain(this.cookieDomain) .httpOnly(this.cookieHttpOnly) .maxAge(!tokenValue.isEmpty() ? this.cookieMaxAge : 0) .path((this.cookiePath != null) ? this.cookiePath : getRequestContext(exchange.getRequest())) - .secure((this.secure != null) ? this.secure : (exchange.getRequest().getSslInfo() != null)); - - this.cookieCustomizer.accept(cookieBuilder); - + .secure((this.secure != null) ? this.secure : (exchange.getRequest().getSslInfo() != null)) + .build(); // @formatter:on - exchange.getResponse().addCookie(cookieBuilder.build()); + exchange.getResponse().addCookie(cookie); }); } @@ -125,9 +107,9 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep } /** - * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead. + * Sets the HttpOnly attribute on the cookie containing the CSRF token + * @param cookieHttpOnly True to mark the cookie as http only. False otherwise. */ - @Deprecated(since = "6.1") public void setCookieHttpOnly(boolean cookieHttpOnly) { this.cookieHttpOnly = cookieHttpOnly; } @@ -168,27 +150,44 @@ public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRep } /** - * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead. + * Sets the cookie domain + * @param cookieDomain The cookie domain */ - @Deprecated(since = "6.1") public void setCookieDomain(String cookieDomain) { this.cookieDomain = cookieDomain; } /** + * Sets the cookie secure flag. If not set, the value depends on + * {@link ServerHttpRequest#getSslInfo()}. + * @param secure The value for the secure flag * @since 5.5 - * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead. */ - @Deprecated(since = "6.1") public void setSecure(boolean secure) { this.secure = secure; } /** + * Sets maximum age in seconds for the cookie that the expected CSRF token is saved to + * and read from. By default maximum age value is -1. + * + *

      + * A positive value indicates that the cookie will expire after that many seconds have + * passed. Note that the value is the maximum age when the cookie will expire, + * not the cookie's current age. + * + *

      + * A negative value means that the cookie is not stored persistently and will be + * deleted when the Web browser exits. + * + *

      + * A zero value causes the cookie to be deleted immediately therefore it is not a + * valid value and in that case an {@link IllegalArgumentException} will be thrown. + * @param cookieMaxAge an integer specifying the maximum age of the cookie in seconds; + * if negative, means the cookie is not stored; if zero, the method throws an + * {@link IllegalArgumentException} * @since 5.8 - * @deprecated Use {@link #setCookieCustomizer(Consumer)} instead. */ - @Deprecated(since = "6.1") public void setCookieMaxAge(int cookieMaxAge) { Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero"); this.cookieMaxAge = cookieMaxAge; diff --git a/web/src/main/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandler.java b/web/src/main/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandler.java index de59167e8c..d361104ecb 100644 --- a/web/src/main/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandler.java +++ b/web/src/main/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -90,7 +90,7 @@ public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfT System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize); byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf); - return (csrfBytes != null) ? Utf8.decode(csrfBytes) : null; + return Utf8.decode(csrfBytes); } private static String createXoredCsrfToken(SecureRandom secureRandom, String token) { @@ -107,9 +107,6 @@ public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfT } private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) { - if (csrfBytes.length < randomBytes.length) { - return null; - } int len = Math.min(randomBytes.length, csrfBytes.length); byte[] xoredCsrf = new byte[len]; System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length); diff --git a/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java b/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java index 2b85e89732..807f96177f 100644 --- a/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/ui/LoginPageGeneratingWebFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2017 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. @@ -101,7 +101,7 @@ public class LoginPageGeneratingWebFilter implements WebFilter { + "rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" " + "crossorigin=\"anonymous\">\n"); page.append(" \n"); + + "rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"); page.append(" \n"); page.append(" \n"); page.append("

      \n"); diff --git a/web/src/main/java/org/springframework/security/web/server/ui/LogoutPageGeneratingWebFilter.java b/web/src/main/java/org/springframework/security/web/server/ui/LogoutPageGeneratingWebFilter.java index b8ad98cd29..b0df82e849 100644 --- a/web/src/main/java/org/springframework/security/web/server/ui/LogoutPageGeneratingWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/ui/LogoutPageGeneratingWebFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,7 +81,7 @@ public class LogoutPageGeneratingWebFilter implements WebFilter { page.append(" \n"); page.append(" \n"); + + "rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"); page.append(" \n"); page.append(" \n"); page.append("
      \n"); diff --git a/web/src/main/java/org/springframework/security/web/session/RequestedUrlRedirectInvalidSessionStrategy.java b/web/src/main/java/org/springframework/security/web/session/RequestedUrlRedirectInvalidSessionStrategy.java index da2d8cf297..2720e72937 100644 --- a/web/src/main/java/org/springframework/security/web/session/RequestedUrlRedirectInvalidSessionStrategy.java +++ b/web/src/main/java/org/springframework/security/web/session/RequestedUrlRedirectInvalidSessionStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -25,7 +25,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; -import org.springframework.util.Assert; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; /** @@ -33,13 +32,12 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; * detected by the {@code SessionManagementFilter}. * * @author Craig Andrews - * @author Mark Chesney */ public final class RequestedUrlRedirectInvalidSessionStrategy implements InvalidSessionStrategy { private final Log logger = LogFactory.getLog(getClass()); - private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); private boolean createNewSession = true; @@ -70,14 +68,4 @@ public final class RequestedUrlRedirectInvalidSessionStrategy implements Invalid this.createNewSession = createNewSession; } - /** - * Sets the redirect strategy to use. The default is {@link DefaultRedirectStrategy}. - * @param redirectStrategy the redirect strategy to use. - * @since 6.2 - */ - public void setRedirectStrategy(RedirectStrategy redirectStrategy) { - Assert.notNull(redirectStrategy, "redirectStrategy cannot be null"); - this.redirectStrategy = redirectStrategy; - } - } diff --git a/web/src/main/java/org/springframework/security/web/util/ThrowableAnalyzer.java b/web/src/main/java/org/springframework/security/web/util/ThrowableAnalyzer.java index 6084434539..2fac52493c 100755 --- a/web/src/main/java/org/springframework/security/web/util/ThrowableAnalyzer.java +++ b/web/src/main/java/org/springframework/security/web/util/ThrowableAnalyzer.java @@ -41,7 +41,7 @@ public class ThrowableAnalyzer { * * @see Throwable#getCause() */ - public static final ThrowableCauseExtractor DEFAULT_EXTRACTOR = Throwable::getCause; + public static final ThrowableCauseExtractor DEFAULT_EXTRACTOR = (throwable) -> throwable.getCause(); /** * Default extractor for {@link InvocationTargetException} instances. diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java index b28b69bbba..383d390fd1 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -17,9 +17,7 @@ package org.springframework.security.web.util.matcher; import java.util.Arrays; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; @@ -68,28 +66,6 @@ public final class AndRequestMatcher implements RequestMatcher { return true; } - /** - * Returns a {@link MatchResult} for this {@link HttpServletRequest}. In the case of a - * match, request variables are a composition of the request variables in underlying - * matchers. In the event that two matchers have the same key, the last key is the one - * propagated. - * @param request the HTTP request - * @return a {@link MatchResult} based on the given HTTP request - * @since 6.1 - */ - @Override - public MatchResult matcher(HttpServletRequest request) { - Map variables = new LinkedHashMap<>(); - for (RequestMatcher matcher : this.requestMatchers) { - MatchResult result = matcher.matcher(request); - if (!result.isMatch()) { - return MatchResult.notMatch(); - } - variables.putAll(result.getVariables()); - } - return MatchResult.match(variables); - } - @Override public String toString() { return "And " + this.requestMatchers; diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java index da9874b4d9..21b6140f70 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java @@ -226,9 +226,10 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria @Override public boolean equals(Object obj) { - if (!(obj instanceof AntPathRequestMatcher other)) { + if (!(obj instanceof AntPathRequestMatcher)) { return false; } + AntPathRequestMatcher other = (AntPathRequestMatcher) obj; return this.pattern.equals(other.pattern) && this.httpMethod == other.httpMethod && this.caseSensitive == other.caseSensitive; } diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/DispatcherTypeRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/DispatcherTypeRequestMatcher.java index 5df416c919..9b0b5ba184 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/DispatcherTypeRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/DispatcherTypeRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 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. @@ -66,7 +66,7 @@ public class DispatcherTypeRequestMatcher implements RequestMatcher { @Override public boolean matches(HttpServletRequest request) { if (this.httpMethod != null && StringUtils.hasText(request.getMethod()) - && this.httpMethod != HttpMethod.valueOf(request.getMethod())) { + && this.httpMethod != HttpMethod.resolve(request.getMethod())) { return false; } return this.dispatcherType == request.getDispatcherType(); diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java index e3add8edf3..3082d311c4 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -62,25 +62,6 @@ public final class OrRequestMatcher implements RequestMatcher { return false; } - /** - * Returns a {@link MatchResult} for this {@link HttpServletRequest}. In the case of a - * match, request variables are any request variables from the first underlying - * matcher. - * @param request the HTTP request - * @return a {@link MatchResult} based on the given HTTP request - * @since 6.1 - */ - @Override - public MatchResult matcher(HttpServletRequest request) { - for (RequestMatcher matcher : this.requestMatchers) { - MatchResult result = matcher.matcher(request); - if (result.isMatch()) { - return result; - } - } - return MatchResult.notMatch(); - } - @Override public String toString() { return "Or " + this.requestMatchers; diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java index 01e0d6be8a..0de0dd54e6 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -116,7 +116,7 @@ public final class RegexRequestMatcher implements RequestMatcher { @Override public boolean matches(HttpServletRequest request) { if (this.httpMethod != null && request.getMethod() != null - && this.httpMethod != HttpMethod.valueOf(request.getMethod())) { + && this.httpMethod != HttpMethod.resolve(request.getMethod())) { return false; } String url = request.getServletPath(); diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatchers.java b/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatchers.java deleted file mode 100644 index a4da49deee..0000000000 --- a/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatchers.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2002-2023 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.web.util.matcher; - -import java.util.List; - -/** - * A factory class to create {@link RequestMatcher} instances. - * - * @author Christian Schuster - * @since 6.1 - */ -public final class RequestMatchers { - - /** - * Creates a {@link RequestMatcher} that matches if at least one of the given - * {@link RequestMatcher}s matches, if matchers are empty then the - * returned matcher never matches. - * @param matchers the {@link RequestMatcher}s to use - * @return the any-of composed {@link RequestMatcher} - * @see OrRequestMatcher - */ - public static RequestMatcher anyOf(RequestMatcher... matchers) { - return (matchers.length > 0) ? new OrRequestMatcher(List.of(matchers)) : (request) -> false; - } - - /** - * Creates a {@link RequestMatcher} that matches if all the given - * {@link RequestMatcher}s match, if matchers are empty then the returned - * matcher always matches. - * @param matchers the {@link RequestMatcher}s to use - * @return the all-of composed {@link RequestMatcher} - * @see AndRequestMatcher - */ - public static RequestMatcher allOf(RequestMatcher... matchers) { - return (matchers.length > 0) ? new AndRequestMatcher(List.of(matchers)) : (request) -> true; - } - - /** - * Creates a {@link RequestMatcher} that matches if the given {@link RequestMatcher} - * does not match. - * @param matcher the {@link RequestMatcher} to use - * @return the inverted {@link RequestMatcher} - */ - public static RequestMatcher not(RequestMatcher matcher) { - return (request) -> !matcher.matches(request); - } - - private RequestMatchers() { - } - -} diff --git a/web/src/test/java/org/springframework/security/web/DefaultRedirectStrategyTests.java b/web/src/test/java/org/springframework/security/web/DefaultRedirectStrategyTests.java index ca8adbcd6e..098309fd97 100644 --- a/web/src/test/java/org/springframework/security/web/DefaultRedirectStrategyTests.java +++ b/web/src/test/java/org/springframework/security/web/DefaultRedirectStrategyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -18,7 +18,6 @@ package org.springframework.security.web; import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -27,7 +26,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException /** * @author Luke Taylor - * @author Mark Chesney * @since 3.0 */ public class DefaultRedirectStrategyTests { @@ -66,21 +64,4 @@ public class DefaultRedirectStrategyTests { .isThrownBy(() -> rds.sendRedirect(request, response, "https://redirectme.somewhere.else")); } - @Test - public void statusCodeIsHandledCorrectly() throws Exception { - // given - DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); - redirectStrategy.setStatusCode(HttpStatus.TEMPORARY_REDIRECT); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - - // when - redirectStrategy.sendRedirect(request, response, "/requested"); - - // then - assertThat(response.isCommitted()).isTrue(); - assertThat(response.getRedirectedUrl()).isEqualTo("/requested"); - assertThat(response.getStatus()).isEqualTo(307); - } - } diff --git a/web/src/test/java/org/springframework/security/web/ObservationFilterChainDecoratorTests.java b/web/src/test/java/org/springframework/security/web/ObservationFilterChainDecoratorTests.java index e95a1bb3eb..7426ea5526 100644 --- a/web/src/test/java/org/springframework/security/web/ObservationFilterChainDecoratorTests.java +++ b/web/src/test/java/org/springframework/security/web/ObservationFilterChainDecoratorTests.java @@ -97,24 +97,6 @@ public class ObservationFilterChainDecoratorTests { assertThat(events.get(1).getName()).isEqualTo(filter.getClass().getSimpleName() + ".after"); } - @Test - void decorateFiltersWhenDefaultsThenUsesEventName() throws Exception { - ObservationHandler handler = mock(ObservationHandler.class); - given(handler.supportsContext(any())).willReturn(true); - ObservationRegistry registry = ObservationRegistry.create(); - registry.observationConfig().observationHandler(handler); - ObservationFilterChainDecorator decorator = new ObservationFilterChainDecorator(registry); - FilterChain chain = mock(FilterChain.class); - Filter filter = new BasicAuthenticationFilter(); - FilterChain decorated = decorator.decorate(chain, List.of(filter)); - decorated.doFilter(new MockHttpServletRequest("GET", "/"), new MockHttpServletResponse()); - ArgumentCaptor event = ArgumentCaptor.forClass(Observation.Event.class); - verify(handler, times(2)).onEvent(event.capture(), any()); - List events = event.getAllValues(); - assertThat(events.get(0).getName()).isEqualTo("authentication.basic.before"); - assertThat(events.get(1).getName()).isEqualTo("authentication.basic.after"); - } - // gh-12787 @Test void decorateFiltersWhenErrorsThenClosesObservationOnlyOnce() throws Exception { @@ -150,13 +132,6 @@ public class ObservationFilterChainDecoratorTests { .isEqualTo(expectedFilterNameTag); } - // gh-13660 - @Test - void observationNamesDoNotContainDashes() { - ObservationFilterChainDecorator.ObservationFilter.OBSERVATION_NAMES.values() - .forEach((name) -> assertThat(name).doesNotContain("-")); - } - static Stream decorateFiltersWhenCompletesThenHasSpringSecurityReachedFilterNameTag() { Filter filterWithName = new BasicAuthenticationFilter(); diff --git a/web/src/test/java/org/springframework/security/web/access/NoOpAccessDeniedHandlerTests.java b/web/src/test/java/org/springframework/security/web/access/NoOpAccessDeniedHandlerTests.java deleted file mode 100644 index 8dab8dfb36..0000000000 --- a/web/src/test/java/org/springframework/security/web/access/NoOpAccessDeniedHandlerTests.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2002-2023 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.web.access; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.Test; - -import org.springframework.security.access.AccessDeniedException; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verifyNoInteractions; - -class NoOpAccessDeniedHandlerTests { - - private final NoOpAccessDeniedHandler handler = new NoOpAccessDeniedHandler(); - - @Test - void handleWhenInvokedThenDoesNothing() throws Exception { - HttpServletRequest request = mock(HttpServletRequest.class); - HttpServletResponse response = mock(HttpServletResponse.class); - AccessDeniedException exception = mock(AccessDeniedException.class); - this.handler.handle(request, response, exception); - verifyNoInteractions(request, response, exception); - } - -} diff --git a/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java b/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java index aa969ce2bb..7914f4f78f 100644 --- a/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java +++ b/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,20 +21,16 @@ import java.util.function.Supplier; import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.authorization.AuthenticatedAuthorizationManager; import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.core.Authentication; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcherEntry; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link RequestMatcherDelegatingAuthorizationManager}. @@ -128,280 +124,4 @@ public class RequestMatcherDelegatingAuthorizationManagerTests { .withMessage("mappingsConsumer cannot be null"); } - @Test - public void mappingsWhenConfiguredAfterAnyRequestThenException() { - assertThatIllegalStateException() - .isThrownBy(() -> RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .authenticated() - .mappings((m) -> m.add(new RequestMatcherEntry<>(AnyRequestMatcher.INSTANCE, - AuthenticatedAuthorizationManager.authenticated())))) - .withMessage("Can't configure mappings after anyRequest"); - } - - @Test - public void addWhenConfiguredAfterAnyRequestThenException() { - assertThatIllegalStateException() - .isThrownBy(() -> RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .authenticated() - .add(AnyRequestMatcher.INSTANCE, AuthenticatedAuthorizationManager.authenticated())) - .withMessage("Can't add mappings after anyRequest"); - } - - @Test - public void requestMatchersWhenConfiguredAfterAnyRequestThenException() { - assertThatIllegalStateException() - .isThrownBy(() -> RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .authenticated() - .requestMatchers(new AntPathRequestMatcher("/authenticated")) - .authenticated() - .build()) - .withMessage("Can't configure requestMatchers after anyRequest"); - } - - @Test - public void anyRequestWhenConfiguredAfterAnyRequestThenException() { - assertThatIllegalStateException() - .isThrownBy(() -> RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .authenticated() - .anyRequest() - .authenticated() - .build()) - .withMessage("Can't configure anyRequest after itself"); - } - - @Test - public void anyRequestWhenPermitAllThenGrantedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .permitAll() - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::anonymousUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isTrue(); - } - - @Test - public void anyRequestWhenDenyAllThenDeniedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .denyAll() - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isFalse(); - } - - @Test - public void authenticatedWhenAuthenticatedUserThenGrantedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .authenticated() - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isTrue(); - } - - @Test - public void authenticatedWhenAnonymousUserThenDeniedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .authenticated() - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::anonymousUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isFalse(); - } - - @Test - public void fullyAuthenticatedWhenAuthenticatedUserThenGrantedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .fullyAuthenticated() - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isTrue(); - } - - @Test - public void fullyAuthenticatedWhenAnonymousUserThenDeniedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .fullyAuthenticated() - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::anonymousUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isFalse(); - } - - @Test - public void fullyAuthenticatedWhenRememberMeUserThenDeniedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .fullyAuthenticated() - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::rememberMeUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isFalse(); - } - - @Test - public void rememberMeWhenRememberMeUserThenGrantedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .rememberMe() - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::rememberMeUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isTrue(); - } - - @Test - public void rememberMeWhenAuthenticatedUserThenDeniedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .rememberMe() - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isFalse(); - } - - @Test - public void anonymousWhenAnonymousUserThenGrantedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .anonymous() - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::anonymousUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isTrue(); - } - - @Test - public void anonymousWhenAuthenticatedUserThenDeniedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .anonymous() - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isFalse(); - } - - @Test - public void hasRoleAdminWhenAuthenticatedUserThenDeniedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .hasRole("ADMIN") - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isFalse(); - } - - @Test - public void hasRoleAdminWhenAuthenticatedAdminThenGrantedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .hasRole("ADMIN") - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isTrue(); - } - - @Test - public void hasAnyRoleUserOrAdminWhenAuthenticatedUserThenGrantedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .hasAnyRole("USER", "ADMIN") - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isTrue(); - } - - @Test - public void hasAnyRoleUserOrAdminWhenAuthenticatedAdminThenGrantedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .hasAnyRole("USER", "ADMIN") - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isTrue(); - } - - @Test - public void hasAnyRoleUserOrAdminWhenAnonymousUserThenDeniedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .hasAnyRole("USER", "ADMIN") - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::anonymousUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isFalse(); - } - - @Test - public void hasAuthorityRoleAdminWhenAuthenticatedUserThenDeniedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .hasAuthority("ROLE_ADMIN") - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isFalse(); - } - - @Test - public void hasAuthorityRoleAdminWhenAuthenticatedAdminThenGrantedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .hasAuthority("ROLE_ADMIN") - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isTrue(); - } - - @Test - public void hasAnyAuthorityRoleUserOrAdminWhenAuthenticatedUserThenGrantedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .hasAnyAuthority("ROLE_USER", "ROLE_ADMIN") - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isTrue(); - } - - @Test - public void hasAnyAuthorityRoleUserOrAdminWhenAuthenticatedAdminThenGrantedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .hasAnyAuthority("ROLE_USER", "ROLE_ADMIN") - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isTrue(); - } - - @Test - public void hasAnyAuthorityRoleUserOrAdminWhenAnonymousUserThenDeniedDecision() { - RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() - .anyRequest() - .hasAnyRole("USER", "ADMIN") - .build(); - AuthorizationDecision decision = manager.check(TestAuthentication::anonymousUser, null); - assertThat(decision).isNotNull(); - assertThat(decision.isGranted()).isFalse(); - } - } diff --git a/web/src/test/java/org/springframework/security/web/authentication/AbstractAuthenticationTargetUrlRequestHandlerTests.java b/web/src/test/java/org/springframework/security/web/authentication/AbstractAuthenticationTargetUrlRequestHandlerTests.java deleted file mode 100644 index 3c721567ea..0000000000 --- a/web/src/test/java/org/springframework/security/web/authentication/AbstractAuthenticationTargetUrlRequestHandlerTests.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2002-2023 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.web.authentication; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Dayan Kodippily - */ -public class AbstractAuthenticationTargetUrlRequestHandlerTests { - - public static final String REQUEST_URI = "https://example.org"; - - public static final String DEFAULT_TARGET_URL = "/defaultTarget"; - - public static final String REFERER_URL = "https://www.springsource.com/"; - - public static final String TARGET_URL = "https://example.org/target"; - - private MockHttpServletRequest request; - - private MockHttpServletResponse response; - - private AbstractAuthenticationTargetUrlRequestHandler handler; - - @BeforeEach - void setUp() { - this.request = new MockHttpServletRequest(); - this.response = new MockHttpServletResponse(); - this.handler = new AbstractAuthenticationTargetUrlRequestHandler() { - @Override - protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) { - return super.determineTargetUrl(request, response); - } - }; - this.handler.setDefaultTargetUrl(DEFAULT_TARGET_URL); - this.request.setRequestURI(REQUEST_URI); - } - - @Test - void returnDefaultTargetUrlIfUseDefaultTargetUrlTrue() { - this.handler.setAlwaysUseDefaultTargetUrl(true); - assertThat(this.handler.determineTargetUrl(this.request, this.response)).isEqualTo(DEFAULT_TARGET_URL); - } - - @Test - void returnTargetUrlParamValueIfParamHasValue() { - this.handler.setTargetUrlParameter("param"); - this.request.setParameter("param", TARGET_URL); - assertThat(this.handler.determineTargetUrl(this.request, this.response)).isEqualTo(TARGET_URL); - } - - @Test - void targetUrlParamValueTakePrecedenceOverRefererIfParamHasValue() { - this.handler.setUseReferer(true); - this.handler.setTargetUrlParameter("param"); - this.request.setParameter("param", TARGET_URL); - assertThat(this.handler.determineTargetUrl(this.request, this.response)).isEqualTo(TARGET_URL); - } - - @Test - void returnDefaultTargetUrlIfTargetUrlParamHasNoValue() { - this.handler.setTargetUrlParameter("param"); - this.request.setParameter("param", ""); - assertThat(this.handler.determineTargetUrl(this.request, this.response)).isEqualTo(DEFAULT_TARGET_URL); - } - - @Test - void returnDefaultTargetUrlIfTargetUrlParamHasNoValueContainsOnlyWhiteSpaces() { - this.handler.setTargetUrlParameter("param"); - this.request.setParameter("param", " "); - assertThat(this.handler.determineTargetUrl(this.request, this.response)).isEqualTo(DEFAULT_TARGET_URL); - } - - @Test - void returnRefererUrlIfUseRefererIsTrue() { - this.handler.setUseReferer(true); - this.request.addHeader("Referer", REFERER_URL); - assertThat(this.handler.determineTargetUrl(this.request, this.response)).isEqualTo(REFERER_URL); - } - - @Test - void returnDefaultTargetUrlIfUseRefererIsFalse() { - this.handler.setUseReferer(false); - this.request.addHeader("Referer", REFERER_URL); - assertThat(this.handler.determineTargetUrl(this.request, this.response)).isEqualTo(DEFAULT_TARGET_URL); - } - -} diff --git a/web/src/test/java/org/springframework/security/web/authentication/NoOpAuthenticationEntryPointTests.java b/web/src/test/java/org/springframework/security/web/authentication/NoOpAuthenticationEntryPointTests.java deleted file mode 100644 index a687f2bd1b..0000000000 --- a/web/src/test/java/org/springframework/security/web/authentication/NoOpAuthenticationEntryPointTests.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2002-2023 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.web.authentication; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.Test; - -import org.springframework.security.core.AuthenticationException; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verifyNoInteractions; - -class NoOpAuthenticationEntryPointTests { - - private final NoOpAuthenticationEntryPoint authenticationEntryPoint = new NoOpAuthenticationEntryPoint(); - - @Test - void commenceWhenInvokedThenDoesNothing() throws Exception { - HttpServletRequest request = mock(HttpServletRequest.class); - HttpServletResponse response = mock(HttpServletResponse.class); - AuthenticationException exception = mock(AuthenticationException.class); - this.authenticationEntryPoint.commence(request, response, exception); - verifyNoInteractions(request, response, exception); - } - -} diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationTokenTests.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationTokenTests.java index e9eb80cc37..8007b4c4c5 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationTokenTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationTokenTests.java @@ -42,7 +42,7 @@ public class PreAuthenticatedAuthenticationTokenTests { assertThat(token.getPrincipal()).isEqualTo(principal); assertThat(token.getCredentials()).isEqualTo(credentials); assertThat(token.getDetails()).isEqualTo(details); - assertThat(token.getAuthorities()).isEmpty(); + assertThat(token.getAuthorities().isEmpty()).isTrue(); } @Test @@ -53,7 +53,7 @@ public class PreAuthenticatedAuthenticationTokenTests { assertThat(token.getPrincipal()).isEqualTo(principal); assertThat(token.getCredentials()).isEqualTo(credentials); assertThat(token.getDetails()).isNull(); - assertThat(token.getAuthorities()).isEmpty(); + assertThat(token.getAuthorities().isEmpty()).isTrue(); } @Test diff --git a/web/src/test/java/org/springframework/security/web/authentication/rememberme/JdbcTokenRepositoryImplTests.java b/web/src/test/java/org/springframework/security/web/authentication/rememberme/JdbcTokenRepositoryImplTests.java index e95e628d1c..2d93da83d0 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/rememberme/JdbcTokenRepositoryImplTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/rememberme/JdbcTokenRepositoryImplTests.java @@ -95,10 +95,10 @@ public class JdbcTokenRepositoryImplTests { PersistentRememberMeToken token = new PersistentRememberMeToken("joeuser", "joesseries", "atoken", currentDate); this.repo.createNewToken(token); Map results = this.template.queryForMap("select * from persistent_logins"); - assertThat(results).containsEntry("last_used", currentDate); - assertThat(results).containsEntry("username", "joeuser"); - assertThat(results).containsEntry("series", "joesseries"); - assertThat(results).containsEntry("token", "atoken"); + assertThat(results.get("last_used")).isEqualTo(currentDate); + assertThat(results.get("username")).isEqualTo("joeuser"); + assertThat(results.get("series")).isEqualTo("joesseries"); + assertThat(results.get("token")).isEqualTo("atoken"); } @Test @@ -157,9 +157,9 @@ public class JdbcTokenRepositoryImplTests { this.repo.updateToken("joesseries", "newtoken", new Date()); Map results = this.template .queryForMap("select * from persistent_logins where series = 'joesseries'"); - assertThat(results).containsEntry("username", "joeuser"); - assertThat(results).containsEntry("series", "joesseries"); - assertThat(results).containsEntry("token", "newtoken"); + assertThat(results.get("username")).isEqualTo("joeuser"); + assertThat(results.get("series")).isEqualTo("joesseries"); + assertThat(results.get("token")).isEqualTo("newtoken"); Date lastUsed = (Date) results.get("last_used"); assertThat(lastUsed.getTime() > ts.getTime()).isTrue(); } diff --git a/web/src/test/java/org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServicesTests.java b/web/src/test/java/org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServicesTests.java index fbe3e245d8..adc216fb3e 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServicesTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServicesTests.java @@ -93,7 +93,7 @@ public class PersistentTokenBasedRememberMeServicesTests { this.services.processAutoLoginCookie(new String[] { "series", "token" }, new MockHttpServletRequest(), response); assertThat(this.repo.getStoredToken().getSeries()).isEqualTo("series"); - assertThat(this.repo.getStoredToken().getTokenValue()).hasSize(16); + assertThat(this.repo.getStoredToken().getTokenValue().length()).isEqualTo(16); String[] cookie = this.services.decodeCookie(response.getCookie("mycookiename").getValue()); assertThat(cookie[0]).isEqualTo("series"); assertThat(cookie[1]).isEqualTo(this.repo.getStoredToken().getTokenValue()); @@ -108,8 +108,8 @@ public class PersistentTokenBasedRememberMeServicesTests { MockHttpServletResponse response = new MockHttpServletResponse(); this.services.loginSuccess(new MockHttpServletRequest(), response, UsernamePasswordAuthenticationToken.unauthenticated("joe", "password")); - assertThat(this.repo.getStoredToken().getSeries()).hasSize(16); - assertThat(this.repo.getStoredToken().getTokenValue()).hasSize(16); + assertThat(this.repo.getStoredToken().getSeries().length()).isEqualTo(16); + assertThat(this.repo.getStoredToken().getTokenValue().length()).isEqualTo(16); String[] cookie = this.services.decodeCookie(response.getCookie("mycookiename").getValue()); assertThat(cookie[0]).isEqualTo(this.repo.getStoredToken().getSeries()); assertThat(cookie[1]).isEqualTo(this.repo.getStoredToken().getTokenValue()); diff --git a/web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java index 00885411a2..91f4403d90 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -45,7 +45,7 @@ public class DefaultLogoutPageGeneratingFilterTests { + " \n" + " \n" + " Confirm Log Out?\n" + " \n" - + " \n" + + " \n" + " \n" + " \n" + "
      \n" + "
      \n" + " \n" diff --git a/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java index faf9f17db4..134a5d46f3 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java @@ -21,7 +21,6 @@ import java.nio.charset.StandardCharsets; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -42,12 +41,9 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.test.web.CodecTestUtils; -import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.util.WebUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -492,57 +488,4 @@ public class BasicAuthenticationFilterTests { assertThat(authenticationRequest.getName()).isEqualTo("rod"); } - @Test - public void doFilterWhenCustomAuthenticationConverterThatIgnoresRequestThenIgnores() throws Exception { - this.filter.setAuthenticationConverter(new TestAuthenticationConverter()); - String token = "rod:koala"; - MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); - request.setServletPath("/ignored"); - FilterChain filterChain = mock(FilterChain.class); - MockHttpServletResponse response = new MockHttpServletResponse(); - this.filter.doFilter(request, response, filterChain); - assertThat(response.getStatus()).isEqualTo(200); - - verify(this.manager, never()).authenticate(any(Authentication.class)); - verify(filterChain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); - verifyNoMoreInteractions(this.manager, filterChain); - } - - @Test - public void doFilterWhenCustomAuthenticationConverterRequestThenAuthenticate() throws Exception { - this.filter.setAuthenticationConverter(new TestAuthenticationConverter()); - String token = "rod:koala"; - MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); - request.setServletPath("/ok"); - FilterChain filterChain = mock(FilterChain.class); - MockHttpServletResponse response = new MockHttpServletResponse(); - this.filter.doFilter(request, response, filterChain); - assertThat(response.getStatus()).isEqualTo(200); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); - assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("rod"); - } - - @Test - public void setAuthenticationConverterWhenNullThenException() { - assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAuthenticationConverter(null)); - } - - static class TestAuthenticationConverter implements AuthenticationConverter { - - private final RequestMatcher matcher = AntPathRequestMatcher.antMatcher("/ignored"); - - private final BasicAuthenticationConverter delegate = new BasicAuthenticationConverter(); - - @Override - public Authentication convert(HttpServletRequest request) { - if (this.matcher.matches(request)) { - return null; - } - return this.delegate.convert(request); - } - - } - } diff --git a/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthUtilsTests.java b/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthUtilsTests.java index 9b9387a15e..9b5345765f 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthUtilsTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthUtilsTests.java @@ -38,16 +38,16 @@ public class DigestAuthUtilsTests { String unsplit = "username=\"rod\", invalidEntryThatHasNoEqualsSign, realm=\"Contacts Realm\", nonce=\"MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ==\", uri=\"/spring-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4\", response=\"38644211cf9ac3da63ab639807e2baff\", qop=auth, nc=00000004, cnonce=\"2b8d329a8571b99a\""; String[] headerEntries = StringUtils.commaDelimitedListToStringArray(unsplit); Map headerMap = DigestAuthUtils.splitEachArrayElementAndCreateMap(headerEntries, "=", "\""); - assertThat(headerMap).containsEntry("username", "rod"); - assertThat(headerMap).containsEntry("realm", "Contacts Realm"); - assertThat(headerMap).containsEntry("nonce", - "MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ=="); - assertThat(headerMap).containsEntry("uri", - "/spring-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4"); - assertThat(headerMap).containsEntry("response", "38644211cf9ac3da63ab639807e2baff"); - assertThat(headerMap).containsEntry("qop", "auth"); - assertThat(headerMap).containsEntry("nc", "00000004"); - assertThat(headerMap).containsEntry("cnonce", "2b8d329a8571b99a"); + assertThat(headerMap.get("username")).isEqualTo("rod"); + assertThat(headerMap.get("realm")).isEqualTo("Contacts Realm"); + assertThat(headerMap.get("nonce")) + .isEqualTo("MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ=="); + assertThat(headerMap.get("uri")) + .isEqualTo("/spring-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4"); + assertThat(headerMap.get("response")).isEqualTo("38644211cf9ac3da63ab639807e2baff"); + assertThat(headerMap.get("qop")).isEqualTo("auth"); + assertThat(headerMap.get("nc")).isEqualTo("00000004"); + assertThat(headerMap.get("cnonce")).isEqualTo("2b8d329a8571b99a"); assertThat(headerMap).hasSize(8); } @@ -56,16 +56,16 @@ public class DigestAuthUtilsTests { String unsplit = "username=\"rod\", realm=\"Contacts Realm\", nonce=\"MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ==\", uri=\"/spring-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4\", response=\"38644211cf9ac3da63ab639807e2baff\", qop=auth, nc=00000004, cnonce=\"2b8d329a8571b99a\""; String[] headerEntries = StringUtils.commaDelimitedListToStringArray(unsplit); Map headerMap = DigestAuthUtils.splitEachArrayElementAndCreateMap(headerEntries, "=", null); - assertThat(headerMap).containsEntry("username", "\"rod\""); - assertThat(headerMap).containsEntry("realm", "\"Contacts Realm\""); - assertThat(headerMap).containsEntry("nonce", - "\"MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ==\""); - assertThat(headerMap).containsEntry("uri", - "\"/spring-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4\""); - assertThat(headerMap).containsEntry("response", "\"38644211cf9ac3da63ab639807e2baff\""); - assertThat(headerMap).containsEntry("qop", "auth"); - assertThat(headerMap).containsEntry("nc", "00000004"); - assertThat(headerMap).containsEntry("cnonce", "\"2b8d329a8571b99a\""); + assertThat(headerMap.get("username")).isEqualTo("\"rod\""); + assertThat(headerMap.get("realm")).isEqualTo("\"Contacts Realm\""); + assertThat(headerMap.get("nonce")) + .isEqualTo("\"MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ==\""); + assertThat(headerMap.get("uri")) + .isEqualTo("\"/spring-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4\""); + assertThat(headerMap.get("response")).isEqualTo("\"38644211cf9ac3da63ab639807e2baff\""); + assertThat(headerMap.get("qop")).isEqualTo("auth"); + assertThat(headerMap.get("nc")).isEqualTo("00000004"); + assertThat(headerMap.get("cnonce")).isEqualTo("\"2b8d329a8571b99a\""); assertThat(headerMap).hasSize(8); } diff --git a/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationEntryPointTests.java b/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationEntryPointTests.java index 761aeed07c..63c74dfd95 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationEntryPointTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationEntryPointTests.java @@ -93,8 +93,8 @@ public class DigestAuthenticationEntryPointTests { String header = response.getHeader("WWW-Authenticate").toString().substring(7); String[] headerEntries = StringUtils.commaDelimitedListToStringArray(header); Map headerMap = DigestAuthUtils.splitEachArrayElementAndCreateMap(headerEntries, "=", "\""); - assertThat(headerMap).containsEntry("realm", "hello"); - assertThat(headerMap).containsEntry("qop", "auth"); + assertThat(headerMap.get("realm")).isEqualTo("hello"); + assertThat(headerMap.get("qop")).isEqualTo("auth"); assertThat(headerMap.get("stale")).isNull(); checkNonceValid(headerMap.get("nonce")); } @@ -116,9 +116,9 @@ public class DigestAuthenticationEntryPointTests { String header = response.getHeader("WWW-Authenticate").toString().substring(7); String[] headerEntries = StringUtils.commaDelimitedListToStringArray(header); Map headerMap = DigestAuthUtils.splitEachArrayElementAndCreateMap(headerEntries, "=", "\""); - assertThat(headerMap).containsEntry("realm", "hello"); - assertThat(headerMap).containsEntry("qop", "auth"); - assertThat(headerMap).containsEntry("stale", "true"); + assertThat(headerMap.get("realm")).isEqualTo("hello"); + assertThat(headerMap.get("qop")).isEqualTo("auth"); + assertThat(headerMap.get("stale")).isEqualTo("true"); checkNonceValid(headerMap.get("nonce")); } diff --git a/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilterTests.java index 230c554fcd..30e72ea4c1 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilterTests.java @@ -149,7 +149,7 @@ public class DigestAuthenticationFilterTests { String header = response.getHeader("WWW-Authenticate").toString().substring(7); String[] headerEntries = StringUtils.commaDelimitedListToStringArray(header); Map headerMap = DigestAuthUtils.splitEachArrayElementAndCreateMap(headerEntries, "=", "\""); - assertThat(headerMap).containsEntry("stale", "true"); + assertThat(headerMap.get("stale")).isEqualTo("true"); } @Test diff --git a/web/src/test/java/org/springframework/security/web/context/AbstractSecurityWebApplicationInitializerTests.java b/web/src/test/java/org/springframework/security/web/context/AbstractSecurityWebApplicationInitializerTests.java index 103716d119..16c3354394 100644 --- a/web/src/test/java/org/springframework/security/web/context/AbstractSecurityWebApplicationInitializerTests.java +++ b/web/src/test/java/org/springframework/security/web/context/AbstractSecurityWebApplicationInitializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.security.web.context; import java.util.Collections; import java.util.EnumSet; import java.util.EventListener; +import java.util.HashSet; import java.util.Set; import jakarta.servlet.DispatcherType; @@ -318,15 +319,14 @@ public class AbstractSecurityWebApplicationInitializerTests { ServletContext context = mock(ServletContext.class); FilterRegistration.Dynamic registration = mock(FilterRegistration.Dynamic.class); ArgumentCaptor proxyCaptor = ArgumentCaptor.forClass(DelegatingFilterProxy.class); - given(context.addFilter(eq("springSecurityFilterChain"), any(DelegatingFilterProxy.class))) - .willReturn(registration); - @SuppressWarnings("unchecked") - ArgumentCaptor> modesCaptor = ArgumentCaptor.forClass(Set.class); + given(context.addFilter(eq("springSecurityFilterChain"), proxyCaptor.capture())).willReturn(registration); + ArgumentCaptor> modesCaptor = ArgumentCaptor + .forClass(new HashSet() { + }.getClass()); + willDoNothing().given(context).setSessionTrackingModes(modesCaptor.capture()); new AbstractSecurityWebApplicationInitializer() { }.onStartup(context); - verify(context).addFilter(eq("springSecurityFilterChain"), proxyCaptor.capture()); assertProxyDefaults(proxyCaptor.getValue()); - verify(context).setSessionTrackingModes(modesCaptor.capture()); Set modes = modesCaptor.getValue(); assertThat(modes).hasSize(1); assertThat(modes).containsExactly(SessionTrackingMode.COOKIE); @@ -337,20 +337,18 @@ public class AbstractSecurityWebApplicationInitializerTests { ServletContext context = mock(ServletContext.class); FilterRegistration.Dynamic registration = mock(FilterRegistration.Dynamic.class); ArgumentCaptor proxyCaptor = ArgumentCaptor.forClass(DelegatingFilterProxy.class); - given(context.addFilter(eq("springSecurityFilterChain"), any(DelegatingFilterProxy.class))) - .willReturn(registration); - @SuppressWarnings("unchecked") - ArgumentCaptor> modesCaptor = ArgumentCaptor.forClass(Set.class); - willDoNothing().given(context).setSessionTrackingModes(any()); + given(context.addFilter(eq("springSecurityFilterChain"), proxyCaptor.capture())).willReturn(registration); + ArgumentCaptor> modesCaptor = ArgumentCaptor + .forClass(new HashSet() { + }.getClass()); + willDoNothing().given(context).setSessionTrackingModes(modesCaptor.capture()); new AbstractSecurityWebApplicationInitializer() { @Override public Set getSessionTrackingModes() { return Collections.singleton(SessionTrackingMode.SSL); } }.onStartup(context); - verify(context).addFilter(eq("springSecurityFilterChain"), proxyCaptor.capture()); assertProxyDefaults(proxyCaptor.getValue()); - verify(context).setSessionTrackingModes(modesCaptor.capture()); Set modes = modesCaptor.getValue(); assertThat(modes).hasSize(1); assertThat(modes).containsExactly(SessionTrackingMode.SSL); diff --git a/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java b/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java index eb66521f89..7d9aae39d0 100644 --- a/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,6 @@ import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.http.HttpHeaders; -import org.springframework.mock.web.MockCookie; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -31,10 +29,9 @@ import static org.springframework.security.web.csrf.CsrfTokenAssert.assertThatCs /** * @author Rob Winch - * @author Alex Montoya * @since 4.1 */ -class CookieCsrfTokenRepositoryTests { +public class CookieCsrfTokenRepositoryTests { CookieCsrfTokenRepository repository; @@ -43,7 +40,7 @@ class CookieCsrfTokenRepositoryTests { MockHttpServletRequest request; @BeforeEach - void setup() { + public void setup() { this.repository = new CookieCsrfTokenRepository(); this.request = new MockHttpServletRequest(); this.response = new MockHttpServletResponse(); @@ -51,7 +48,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void generateToken() { + public void generateToken() { CsrfToken generateToken = this.repository.generateToken(this.request); assertThat(generateToken).isNotNull(); assertThat(generateToken.getHeaderName()).isEqualTo(CookieCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME); @@ -60,7 +57,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void generateTokenCustom() { + public void generateTokenCustom() { String headerName = "headerName"; String parameterName = "paramName"; this.repository.setHeaderName(headerName); @@ -73,7 +70,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveToken() { + public void saveToken() { CsrfToken token = this.repository.generateToken(this.request); this.repository.saveToken(token, this.request, this.response); Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); @@ -82,11 +79,11 @@ class CookieCsrfTokenRepositoryTests { assertThat(tokenCookie.getPath()).isEqualTo(this.request.getContextPath()); assertThat(tokenCookie.getSecure()).isEqualTo(this.request.isSecure()); assertThat(tokenCookie.getValue()).isEqualTo(token.getToken()); - assertThat(tokenCookie.isHttpOnly()).isTrue(); + assertThat(tokenCookie.isHttpOnly()).isEqualTo(true); } @Test - void saveTokenSecure() { + public void saveTokenSecure() { this.request.setSecure(true); CsrfToken token = this.repository.generateToken(this.request); this.repository.saveToken(token, this.request, this.response); @@ -95,7 +92,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveTokenSecureFlagTrue() { + public void saveTokenSecureFlagTrue() { this.request.setSecure(false); this.repository.setSecure(Boolean.TRUE); CsrfToken token = this.repository.generateToken(this.request); @@ -105,17 +102,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveTokenSecureFlagTrueUsingCustomizer() { - this.request.setSecure(false); - this.repository.setCookieCustomizer((customizer) -> customizer.secure(Boolean.TRUE)); - CsrfToken token = this.repository.generateToken(this.request); - this.repository.saveToken(token, this.request, this.response); - Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); - assertThat(tokenCookie.getSecure()).isTrue(); - } - - @Test - void saveTokenSecureFlagFalse() { + public void saveTokenSecureFlagFalse() { this.request.setSecure(true); this.repository.setSecure(Boolean.FALSE); CsrfToken token = this.repository.generateToken(this.request); @@ -125,17 +112,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveTokenSecureFlagFalseUsingCustomizer() { - this.request.setSecure(true); - this.repository.setCookieCustomizer((customizer) -> customizer.secure(Boolean.FALSE)); - CsrfToken token = this.repository.generateToken(this.request); - this.repository.saveToken(token, this.request, this.response); - Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); - assertThat(tokenCookie.getSecure()).isFalse(); - } - - @Test - void saveTokenNull() { + public void saveTokenNull() { this.request.setSecure(true); this.repository.saveToken(null, this.request, this.response); Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); @@ -147,7 +124,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveTokenHttpOnlyTrue() { + public void saveTokenHttpOnlyTrue() { this.repository.setCookieHttpOnly(true); CsrfToken token = this.repository.generateToken(this.request); this.repository.saveToken(token, this.request, this.response); @@ -156,16 +133,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveTokenHttpOnlyTrueUsingCustomizer() { - this.repository.setCookieCustomizer((customizer) -> customizer.httpOnly(true)); - CsrfToken token = this.repository.generateToken(this.request); - this.repository.saveToken(token, this.request, this.response); - Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); - assertThat(tokenCookie.isHttpOnly()).isTrue(); - } - - @Test - void saveTokenHttpOnlyFalse() { + public void saveTokenHttpOnlyFalse() { this.repository.setCookieHttpOnly(false); CsrfToken token = this.repository.generateToken(this.request); this.repository.saveToken(token, this.request, this.response); @@ -174,16 +142,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveTokenHttpOnlyFalseUsingCustomizer() { - this.repository.setCookieCustomizer((customizer) -> customizer.httpOnly(false)); - CsrfToken token = this.repository.generateToken(this.request); - this.repository.saveToken(token, this.request, this.response); - Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); - assertThat(tokenCookie.isHttpOnly()).isFalse(); - } - - @Test - void saveTokenWithHttpOnlyFalse() { + public void saveTokenWithHttpOnlyFalse() { this.repository = CookieCsrfTokenRepository.withHttpOnlyFalse(); CsrfToken token = this.repository.generateToken(this.request); this.repository.saveToken(token, this.request, this.response); @@ -192,7 +151,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveTokenCustomPath() { + public void saveTokenCustomPath() { String customPath = "/custompath"; this.repository.setCookiePath(customPath); CsrfToken token = this.repository.generateToken(this.request); @@ -202,7 +161,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveTokenEmptyCustomPath() { + public void saveTokenEmptyCustomPath() { String customPath = ""; this.repository.setCookiePath(customPath); CsrfToken token = this.repository.generateToken(this.request); @@ -212,7 +171,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveTokenNullCustomPath() { + public void saveTokenNullCustomPath() { String customPath = null; this.repository.setCookiePath(customPath); CsrfToken token = this.repository.generateToken(this.request); @@ -222,7 +181,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveTokenWithCookieDomain() { + public void saveTokenWithCookieDomain() { String domainName = "example.com"; this.repository.setCookieDomain(domainName); CsrfToken token = this.repository.generateToken(this.request); @@ -232,17 +191,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveTokenWithCookieDomainUsingCustomizer() { - String domainName = "example.com"; - this.repository.setCookieCustomizer((customizer) -> customizer.domain(domainName)); - CsrfToken token = this.repository.generateToken(this.request); - this.repository.saveToken(token, this.request, this.response); - Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); - assertThat(tokenCookie.getDomain()).isEqualTo(domainName); - } - - @Test - void saveTokenWithCookieMaxAge() { + public void saveTokenWithCookieMaxAge() { int maxAge = 1200; this.repository.setCookieMaxAge(maxAge); CsrfToken token = this.repository.generateToken(this.request); @@ -252,75 +201,24 @@ class CookieCsrfTokenRepositoryTests { } @Test - void saveTokenWithCookieMaxAgeUsingCustomizer() { - int maxAge = 1200; - this.repository.setCookieCustomizer((customizer) -> customizer.maxAge(maxAge)); - CsrfToken token = this.repository.generateToken(this.request); - this.repository.saveToken(token, this.request, this.response); - Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); - assertThat(tokenCookie.getMaxAge()).isEqualTo(maxAge); - } - - @Test - void saveTokenWithSameSiteNull() { - String sameSitePolicy = null; - this.repository.setCookieCustomizer((customizer) -> customizer.sameSite(sameSitePolicy)); - CsrfToken token = this.repository.generateToken(this.request); - this.repository.saveToken(token, this.request, this.response); - Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); - assertThat(((MockCookie) tokenCookie).getSameSite()).isNull(); - } - - @Test - void saveTokenWithSameSiteStrict() { - String sameSitePolicy = "Strict"; - this.repository.setCookieCustomizer((customizer) -> customizer.sameSite(sameSitePolicy)); - CsrfToken token = this.repository.generateToken(this.request); - this.repository.saveToken(token, this.request, this.response); - Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); - assertThat(((MockCookie) tokenCookie).getSameSite()).isEqualTo(sameSitePolicy); - } - - @Test - void saveTokenWithSameSiteLax() { - String sameSitePolicy = "Lax"; - this.repository.setCookieCustomizer((customizer) -> customizer.sameSite(sameSitePolicy)); - CsrfToken token = this.repository.generateToken(this.request); - this.repository.saveToken(token, this.request, this.response); - Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); - assertThat(((MockCookie) tokenCookie).getSameSite()).isEqualTo(sameSitePolicy); - } - - // gh-13075 - @Test - void saveTokenWithExistingSetCookieThenDoesNotOverwrite() { - this.response.setHeader(HttpHeaders.SET_COOKIE, "MyCookie=test"); - this.repository = new CookieCsrfTokenRepository(); - CsrfToken token = this.repository.generateToken(this.request); - this.repository.saveToken(token, this.request, this.response); - assertThat(this.response.getCookie("MyCookie")).isNotNull(); - assertThat(this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME)).isNotNull(); - } - - @Test - void loadTokenNoCookiesNull() { + public void loadTokenNoCookiesNull() { assertThat(this.repository.loadToken(this.request)).isNull(); } @Test - void loadTokenCookieIncorrectNameNull() { + public void loadTokenCookieIncorrectNameNull() { this.request.setCookies(new Cookie("other", "name")); assertThat(this.repository.loadToken(this.request)).isNull(); } @Test - void loadTokenCookieValueEmptyString() { + public void loadTokenCookieValueEmptyString() { this.request.setCookies(new Cookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, "")); assertThat(this.repository.loadToken(this.request)).isNull(); } @Test - void loadToken() { + public void loadToken() { CsrfToken generateToken = this.repository.generateToken(this.request); this.request .setCookies(new Cookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, generateToken.getToken())); @@ -332,7 +230,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void loadTokenCustom() { + public void loadTokenCustom() { String cookieName = "cookieName"; String value = "value"; String headerName = "headerName"; @@ -349,7 +247,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void loadDeferredTokenWhenDoesNotExistThenGeneratedAndSaved() { + public void loadDeferredTokenWhenDoesNotExistThenGeneratedAndSaved() { DeferredCsrfToken deferredCsrfToken = this.repository.loadDeferredToken(this.request, this.response); CsrfToken csrfToken = deferredCsrfToken.get(); assertThat(csrfToken).isNotNull(); @@ -365,7 +263,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void loadDeferredTokenWhenExistsAndNullSavedThenGeneratedAndSaved() { + public void loadDeferredTokenWhenExistsAndNullSavedThenGeneratedAndSaved() { CsrfToken generatedToken = this.repository.generateToken(this.request); this.request .setCookies(new Cookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, generatedToken.getToken())); @@ -378,7 +276,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void loadDeferredTokenWhenExistsAndNullSavedAndNonNullSavedThenLoaded() { + public void loadDeferredTokenWhenExistsAndNullSavedAndNonNullSavedThenLoaded() { CsrfToken generatedToken = this.repository.generateToken(this.request); this.request .setCookies(new Cookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, generatedToken.getToken())); @@ -391,7 +289,7 @@ class CookieCsrfTokenRepositoryTests { } @Test - void loadDeferredTokenWhenExistsThenLoaded() { + public void loadDeferredTokenWhenExistsThenLoaded() { CsrfToken generatedToken = this.repository.generateToken(this.request); this.request .setCookies(new Cookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, generatedToken.getToken())); @@ -402,57 +300,22 @@ class CookieCsrfTokenRepositoryTests { } @Test - void cookieCustomizer() { - String domainName = "example.com"; - String customPath = "/custompath"; - String sameSitePolicy = "Strict"; - this.repository.setCookieCustomizer((customizer) -> { - customizer.domain(domainName); - customizer.secure(false); - customizer.path(customPath); - customizer.sameSite(sameSitePolicy); - }); - CsrfToken token = this.repository.generateToken(this.request); - this.repository.saveToken(token, this.request, this.response); - Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); - assertThat(tokenCookie).isNotNull(); - assertThat(tokenCookie.getMaxAge()).isEqualTo(-1); - assertThat(tokenCookie.getDomain()).isEqualTo(domainName); - assertThat(tokenCookie.getPath()).isEqualTo(customPath); - assertThat(tokenCookie.isHttpOnly()).isEqualTo(Boolean.TRUE); - assertThat(((MockCookie) tokenCookie).getSameSite()).isEqualTo(sameSitePolicy); - } - - // gh-13659 - @Test - void withHttpOnlyFalseWhenCookieCustomizerThenStillDefaultsToFalse() { - CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse(); - repository.setCookieCustomizer((customizer) -> customizer.maxAge(1000)); - CsrfToken token = repository.generateToken(this.request); - repository.saveToken(token, this.request, this.response); - Cookie tokenCookie = this.response.getCookie(CookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME); - assertThat(tokenCookie).isNotNull(); - assertThat(tokenCookie.getMaxAge()).isEqualTo(1000); - assertThat(tokenCookie.isHttpOnly()).isEqualTo(Boolean.FALSE); - } - - @Test - void setCookieNameNullIllegalArgumentException() { + public void setCookieNameNullIllegalArgumentException() { assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setCookieName(null)); } @Test - void setParameterNameNullIllegalArgumentException() { + public void setParameterNameNullIllegalArgumentException() { assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setParameterName(null)); } @Test - void setHeaderNameNullIllegalArgumentException() { + public void setHeaderNameNullIllegalArgumentException() { assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setHeaderName(null)); } @Test - void setCookieMaxAgeZeroIllegalArgumentException() { + public void setCookieMaxAgeZeroIllegalArgumentException() { assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setCookieMaxAge(0)); } diff --git a/web/src/test/java/org/springframework/security/web/csrf/XorCsrfTokenRequestAttributeHandlerTests.java b/web/src/test/java/org/springframework/security/web/csrf/XorCsrfTokenRequestAttributeHandlerTests.java index 6f50862411..97e1d67cae 100644 --- a/web/src/test/java/org/springframework/security/web/csrf/XorCsrfTokenRequestAttributeHandlerTests.java +++ b/web/src/test/java/org/springframework/security/web/csrf/XorCsrfTokenRequestAttributeHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -208,14 +208,6 @@ public class XorCsrfTokenRequestAttributeHandlerTests { assertThat(tokenValue).isEqualTo(this.token.getToken()); } - @Test - public void resolveCsrfTokenIsInvalidThenReturnsNull() { - this.request.setParameter(this.token.getParameterName(), XOR_CSRF_TOKEN_VALUE); - CsrfToken csrfToken = new DefaultCsrfToken("headerName", "paramName", "a"); - String tokenValue = this.handler.resolveCsrfTokenValue(this.request, csrfToken); - assertThat(tokenValue).isNull(); - } - private static Answer fillByteArray() { return (invocation) -> { byte[] bytes = invocation.getArgument(0); diff --git a/web/src/test/java/org/springframework/security/web/header/writers/CacheControlHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/CacheControlHeadersWriterTests.java index b01fd9ce12..fd7c8feae0 100644 --- a/web/src/test/java/org/springframework/security/web/header/writers/CacheControlHeadersWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/header/writers/CacheControlHeadersWriterTests.java @@ -47,7 +47,7 @@ public class CacheControlHeadersWriterTests { @Test public void writeHeaders() { this.writer.writeHeaders(this.request, this.response); - assertThat(this.response.getHeaderNames()).hasSize(3); + assertThat(this.response.getHeaderNames().size()).isEqualTo(3); assertThat(this.response.getHeaderValues("Cache-Control")) .containsOnly("no-cache, no-store, max-age=0, must-revalidate"); assertThat(this.response.getHeaderValues("Pragma")).containsOnly("no-cache"); diff --git a/web/src/test/java/org/springframework/security/web/header/writers/HstsHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/HstsHeaderWriterTests.java index 7e2c33a443..616e562827 100644 --- a/web/src/test/java/org/springframework/security/web/header/writers/HstsHeaderWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/header/writers/HstsHeaderWriterTests.java @@ -112,7 +112,7 @@ public class HstsHeaderWriterTests { public void writeHeadersInsecureRequestDoesNotWriteHeader() { this.request.setSecure(false); this.writer.writeHeaders(this.request, this.response); - assertThat(this.response.getHeaderNames()).isEmpty(); + assertThat(this.response.getHeaderNames().isEmpty()).isTrue(); } @Test diff --git a/web/src/test/java/org/springframework/security/web/header/writers/frameoptions/FrameOptionsHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/frameoptions/FrameOptionsHeaderWriterTests.java index 0358f11983..27b47758b0 100644 --- a/web/src/test/java/org/springframework/security/web/header/writers/frameoptions/FrameOptionsHeaderWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/header/writers/frameoptions/FrameOptionsHeaderWriterTests.java @@ -72,7 +72,7 @@ public class FrameOptionsHeaderWriterTests { public void writeHeadersAllowFromReturnsNull() { this.writer = new XFrameOptionsHeaderWriter(this.strategy); this.writer.writeHeaders(this.request, this.response); - assertThat(this.response.getHeaderNames()).isEmpty(); + assertThat(this.response.getHeaderNames().isEmpty()).isTrue(); } @Test diff --git a/web/src/test/java/org/springframework/security/web/method/ResolvableMethod.java b/web/src/test/java/org/springframework/security/web/method/ResolvableMethod.java index 12947d5f2f..834febc6ed 100644 --- a/web/src/test/java/org/springframework/security/web/method/ResolvableMethod.java +++ b/web/src/test/java/org/springframework/security/web/method/ResolvableMethod.java @@ -40,7 +40,7 @@ import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.Factory; import org.springframework.cglib.proxy.MethodProxy; -import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.MethodIntrospector; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; @@ -131,7 +131,7 @@ public final class ResolvableMethod { private static final SpringObjenesis objenesis = new SpringObjenesis(); - private static final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); + private static final ParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); private final Method method; diff --git a/web/src/test/java/org/springframework/security/web/savedrequest/DefaultSavedRequestTests.java b/web/src/test/java/org/springframework/security/web/savedrequest/DefaultSavedRequestTests.java index b38b97c680..f010a6521d 100644 --- a/web/src/test/java/org/springframework/security/web/savedrequest/DefaultSavedRequestTests.java +++ b/web/src/test/java/org/springframework/security/web/savedrequest/DefaultSavedRequestTests.java @@ -45,7 +45,7 @@ public class DefaultSavedRequestTests { MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader("If-None-Match", "somehashvalue"); DefaultSavedRequest saved = new DefaultSavedRequest(request, new MockPortResolver(8080, 8443)); - assertThat(saved.getHeaderValues("if-none-match")).isEmpty(); + assertThat(saved.getHeaderValues("if-none-match").isEmpty()).isTrue(); } // SEC-3082 diff --git a/web/src/test/java/org/springframework/security/web/savedrequest/SavedRequestAwareWrapperTests.java b/web/src/test/java/org/springframework/security/web/savedrequest/SavedRequestAwareWrapperTests.java index 3b9357c878..9f340ea736 100644 --- a/web/src/test/java/org/springframework/security/web/savedrequest/SavedRequestAwareWrapperTests.java +++ b/web/src/test/java/org/springframework/security/web/savedrequest/SavedRequestAwareWrapperTests.java @@ -24,7 +24,6 @@ import java.util.Locale; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.web.PortResolverImpl; @@ -43,21 +42,22 @@ public class SavedRequestAwareWrapperTests { @Test public void savedRequestCookiesAreIgnored() { MockHttpServletRequest newRequest = new MockHttpServletRequest(); - newRequest.setCookies(new Cookie("cookie", "fromnew")); + newRequest.setCookies(new Cookie[] { new Cookie("cookie", "fromnew") }); MockHttpServletRequest savedRequest = new MockHttpServletRequest(); - savedRequest.setCookies(new Cookie("cookie", "fromsaved")); + savedRequest.setCookies(new Cookie[] { new Cookie("cookie", "fromsaved") }); SavedRequestAwareWrapper wrapper = createWrapper(savedRequest, newRequest); assertThat(wrapper.getCookies()).hasSize(1); assertThat(wrapper.getCookies()[0].getValue()).isEqualTo("fromnew"); } @Test + @SuppressWarnings("unchecked") public void savedRequesthHeaderIsReturnedIfSavedRequestIsSet() { MockHttpServletRequest savedRequest = new MockHttpServletRequest(); savedRequest.addHeader("header", "savedheader"); SavedRequestAwareWrapper wrapper = createWrapper(savedRequest, new MockHttpServletRequest()); assertThat(wrapper.getHeader("nonexistent")).isNull(); - Enumeration headers = wrapper.getHeaders("nonexistent"); + Enumeration headers = wrapper.getHeaders("nonexistent"); assertThat(headers.hasMoreElements()).isFalse(); assertThat(wrapper.getHeader("Header")).isEqualTo("savedheader"); headers = wrapper.getHeaders("heaDer"); @@ -97,7 +97,7 @@ public class SavedRequestAwareWrapperTests { SavedRequestAwareWrapper wrapper = createWrapper(savedRequest, wrappedRequest); assertThat(wrapper.getParameterValues("action")).hasSize(1); assertThat(wrapper.getParameterMap()).hasSize(1); - assertThat(wrapper.getParameterMap().get("action")).hasSize(1); + assertThat(((String[]) wrapper.getParameterMap().get("action"))).hasSize(1); } @Test @@ -127,7 +127,7 @@ public class SavedRequestAwareWrapperTests { wrappedRequest.setParameter("action", "bar"); assertThat(wrapper.getParameterValues("action")).isEqualTo(new Object[] { "bar", "foo" }); // Check map is consistent - String[] valuesFromMap = wrapper.getParameterMap().get("action"); + String[] valuesFromMap = (String[]) wrapper.getParameterMap().get("action"); assertThat(valuesFromMap).hasSize(2); assertThat(valuesFromMap[0]).isEqualTo("bar"); } @@ -169,13 +169,4 @@ public class SavedRequestAwareWrapperTests { assertThat(wrapper.getIntHeader("nonexistent")).isEqualTo(-1); } - @Test - public void correctContentTypeIsReturned() { - MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/notused"); - request.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE); - - SavedRequestAwareWrapper wrapper = createWrapper(request, new MockHttpServletRequest("GET", "/notused")); - assertThat(wrapper.getContentType()).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED_VALUE); - } - } diff --git a/web/src/test/java/org/springframework/security/web/server/context/ReactorContextWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/context/ReactorContextWebFilterTests.java index e5e099b6ea..103c64bbf7 100644 --- a/web/src/test/java/org/springframework/security/web/server/context/ReactorContextWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/context/ReactorContextWebFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,23 +17,17 @@ package org.springframework.security.web.server.context; import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; import reactor.util.context.Context; -import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.security.core.Authentication; @@ -125,32 +119,4 @@ public class ReactorContextWebFilterTests { StepVerifier.create(filter).expectAccessibleContext().hasKey(contextKey).then().verifyComplete(); } - @Test - public void filterWhenThreadFactoryIsPlatformThenSecurityContextLoaded() { - ThreadFactory threadFactory = Executors.defaultThreadFactory(); - assertSecurityContextLoaded(threadFactory); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void filterWhenThreadFactoryIsVirtualThenSecurityContextLoaded() { - ThreadFactory threadFactory = new VirtualThreadTaskExecutor().getVirtualThreadFactory(); - assertSecurityContextLoaded(threadFactory); - } - - private void assertSecurityContextLoaded(ThreadFactory threadFactory) { - SecurityContextImpl context = new SecurityContextImpl(this.principal); - given(this.repository.load(any())).willReturn(Mono.just(context)); - // @formatter:off - WebFilter subscribeOnThreadFactory = (exchange, chain) -> chain.filter(exchange) - .subscribeOn(Schedulers.newSingle(threadFactory)); - WebFilter assertSecurityContext = (exchange, chain) -> ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) - .doOnSuccess((authentication) -> assertThat(authentication).isSameAs(this.principal)) - .then(chain.filter(exchange)); - // @formatter:on - this.handler = WebTestHandler.bindToWebFilters(subscribeOnThreadFactory, this.filter, assertSecurityContext); - this.handler.exchange(this.exchange); - } - } diff --git a/web/src/test/java/org/springframework/security/web/server/context/SecurityContextServerWebExchangeWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/context/SecurityContextServerWebExchangeWebFilterTests.java index 2d8c88fe13..dd0a3f30c2 100644 --- a/web/src/test/java/org/springframework/security/web/server/context/SecurityContextServerWebExchangeWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/context/SecurityContextServerWebExchangeWebFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,25 +17,17 @@ package org.springframework.security.web.server.context; import java.util.Collections; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; -import org.junit.jupiter.api.condition.JRE; import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; -import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.ReactiveSecurityContextHolder; -import org.springframework.security.test.web.reactive.server.WebTestHandler; import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilter; import org.springframework.web.server.handler.DefaultWebFilterChain; import static org.assertj.core.api.Assertions.assertThat; @@ -88,31 +80,4 @@ public class SecurityContextServerWebExchangeWebFilterTests { StepVerifier.create(result).verifyComplete(); } - @Test - public void filterWhenThreadFactoryIsPlatformThenContextPopulated() { - ThreadFactory threadFactory = Executors.defaultThreadFactory(); - assertPrincipalPopulated(threadFactory); - } - - @Test - @DisabledOnJre(JRE.JAVA_17) - public void filterWhenThreadFactoryIsVirtualThenContextPopulated() { - ThreadFactory threadFactory = new VirtualThreadTaskExecutor().getVirtualThreadFactory(); - assertPrincipalPopulated(threadFactory); - } - - private void assertPrincipalPopulated(ThreadFactory threadFactory) { - // @formatter:off - WebFilter subscribeOnThreadFactory = (exchange, chain) -> chain.filter(exchange) - .contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.principal)) - .subscribeOn(Schedulers.newSingle(threadFactory)); - WebFilter assertPrincipal = (exchange, chain) -> exchange.getPrincipal() - .doOnSuccess((principal) -> assertThat(principal).isSameAs(this.principal)) - .then(chain.filter(exchange)); - // @formatter:on - WebTestHandler handler = WebTestHandler.bindToWebFilters(subscribeOnThreadFactory, this.filter, - assertPrincipal); - handler.exchange(this.exchange); - } - } diff --git a/web/src/test/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepositoryTests.java b/web/src/test/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepositoryTests.java index 1aa89f21a8..3507c9122c 100644 --- a/web/src/test/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepositoryTests.java @@ -18,7 +18,6 @@ package org.springframework.security.web.server.csrf; import java.security.cert.X509Certificate; import java.time.Duration; -import java.time.temporal.ChronoUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -36,10 +35,9 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Eric Deandrea * @author Thomas Vitale * @author Alonso Araya - * @author Alex Montoya * @since 5.1 */ -class CookieServerCsrfTokenRepositoryTests { +public class CookieServerCsrfTokenRepositoryTests { private CookieServerCsrfTokenRepository csrfTokenRepository; @@ -63,122 +61,78 @@ class CookieServerCsrfTokenRepositoryTests { private String expectedCookieValue = "csrfToken"; - private String expectedSameSitePolicy = null; - @BeforeEach - void setUp() { + public void setUp() { this.csrfTokenRepository = new CookieServerCsrfTokenRepository(); this.request = MockServerHttpRequest.get("/someUri"); } @Test - void generateTokenWhenDefaultThenDefaults() { + public void generateTokenWhenDefaultThenDefaults() { generateTokenAndAssertExpectedValues(); } @Test - void generateTokenWhenCustomHeaderThenCustomHeader() { + public void generateTokenWhenCustomHeaderThenCustomHeader() { setExpectedHeaderName("someHeader"); generateTokenAndAssertExpectedValues(); } @Test - void generateTokenWhenCustomParameterThenCustomParameter() { + public void generateTokenWhenCustomParameterThenCustomParameter() { setExpectedParameterName("someParam"); generateTokenAndAssertExpectedValues(); } @Test - void generateTokenWhenCustomHeaderAndParameterThenCustomHeaderAndParameter() { + public void generateTokenWhenCustomHeaderAndParameterThenCustomHeaderAndParameter() { setExpectedHeaderName("someHeader"); setExpectedParameterName("someParam"); generateTokenAndAssertExpectedValues(); } @Test - void saveTokenWhenNoSubscriptionThenNotWritten() { + public void saveTokenWhenNoSubscriptionThenNotWritten() { MockServerWebExchange exchange = MockServerWebExchange.from(this.request); this.csrfTokenRepository.saveToken(exchange, createToken()); assertThat(exchange.getResponse().getCookies().getFirst(this.expectedCookieName)).isNull(); } @Test - void saveTokenWhenDefaultThenDefaults() { + public void saveTokenWhenDefaultThenDefaults() { saveAndAssertExpectedValues(createToken()); } @Test - void saveTokenWhenNullThenDeletes() { + public void saveTokenWhenNullThenDeletes() { saveAndAssertExpectedValues(null); } @Test - void saveTokenWhenHttpOnlyFalseThenHttpOnlyFalse() { + public void saveTokenWhenHttpOnlyFalseThenHttpOnlyFalse() { setExpectedHttpOnly(false); saveAndAssertExpectedValues(createToken()); } @Test - void saveTokenWhenCookieMaxAgeThenCookieMaxAge() { + public void saveTokenWhenCookieMaxAgeThenCookieMaxAge() { setExpectedCookieMaxAge(3600); saveAndAssertExpectedValues(createToken()); } @Test - void saveTokenWhenSameSiteThenCookieSameSite() { - setExpectedSameSitePolicy("Lax"); - saveAndAssertExpectedValues(createToken()); - } - - @Test - void saveTokenWhenCustomPropertiesThenCustomProperties() { + public void saveTokenWhenCustomPropertiesThenCustomProperties() { setExpectedDomain("spring.io"); setExpectedCookieName("csrfCookie"); setExpectedPath("/some/path"); setExpectedHeaderName("headerName"); setExpectedParameterName("paramName"); - setExpectedSameSitePolicy("Strict"); setExpectedCookieMaxAge(3600); saveAndAssertExpectedValues(createToken()); } @Test - void saveTokenWhenCustomPropertiesThenCustomPropertiesUsingCustomizer() { - String expectedDomain = "spring.io"; - int expectedMaxAge = 3600; - String expectedPath = "/some/path"; - String expectedSameSite = "Strict"; - - setExpectedCookieName("csrfCookie"); - - setExpectedHeaderName("headerName"); - setExpectedParameterName("paramName"); - - CsrfToken token = createToken(); - - this.csrfTokenRepository.setCookieCustomizer((customizer) -> { - customizer.domain(expectedDomain); - customizer.maxAge(expectedMaxAge); - customizer.path(expectedPath); - customizer.sameSite(expectedSameSite); - }); - - MockServerWebExchange exchange = MockServerWebExchange.from(this.request); - this.csrfTokenRepository.saveToken(exchange, token).block(); - ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName); - assertThat(cookie).isNotNull(); - assertThat(cookie.getMaxAge()).isEqualTo(Duration.of(expectedMaxAge, ChronoUnit.SECONDS)); - assertThat(cookie.getDomain()).isEqualTo(expectedDomain); - assertThat(cookie.getPath()).isEqualTo(expectedPath); - assertThat(cookie.getSameSite()).isEqualTo(expectedSameSite); - assertThat(cookie.isSecure()).isEqualTo(this.expectedSecure); - assertThat(cookie.isHttpOnly()).isEqualTo(this.expectedHttpOnly); - assertThat(cookie.getName()).isEqualTo(this.expectedCookieName); - assertThat(cookie.getValue()).isEqualTo(this.expectedCookieValue); - } - - @Test - void saveTokenWhenSslInfoPresentThenSecure() { + public void saveTokenWhenSslInfoPresentThenSecure() { this.request.sslInfo(new MockSslInfo()); MockServerWebExchange exchange = MockServerWebExchange.from(this.request); this.csrfTokenRepository.saveToken(exchange, createToken()).block(); @@ -188,7 +142,7 @@ class CookieServerCsrfTokenRepositoryTests { } @Test - void saveTokenWhenSslInfoNullThenNotSecure() { + public void saveTokenWhenSslInfoNullThenNotSecure() { MockServerWebExchange exchange = MockServerWebExchange.from(this.request); this.csrfTokenRepository.saveToken(exchange, createToken()).block(); ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName); @@ -197,7 +151,7 @@ class CookieServerCsrfTokenRepositoryTests { } @Test - void saveTokenWhenSecureFlagTrueThenSecure() { + public void saveTokenWhenSecureFlagTrueThenSecure() { MockServerWebExchange exchange = MockServerWebExchange.from(this.request); this.csrfTokenRepository.setSecure(true); this.csrfTokenRepository.saveToken(exchange, createToken()).block(); @@ -207,17 +161,7 @@ class CookieServerCsrfTokenRepositoryTests { } @Test - void saveTokenWhenSecureFlagTrueThenSecureUsingCustomizer() { - MockServerWebExchange exchange = MockServerWebExchange.from(this.request); - this.csrfTokenRepository.setCookieCustomizer((customizer) -> customizer.secure(true)); - this.csrfTokenRepository.saveToken(exchange, createToken()).block(); - ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName); - assertThat(cookie).isNotNull(); - assertThat(cookie.isSecure()).isTrue(); - } - - @Test - void saveTokenWhenSecureFlagFalseThenNotSecure() { + public void saveTokenWhenSecureFlagFalseThenNotSecure() { MockServerWebExchange exchange = MockServerWebExchange.from(this.request); this.csrfTokenRepository.setSecure(false); this.csrfTokenRepository.saveToken(exchange, createToken()).block(); @@ -227,17 +171,7 @@ class CookieServerCsrfTokenRepositoryTests { } @Test - void saveTokenWhenSecureFlagFalseThenNotSecureUsingCustomizer() { - MockServerWebExchange exchange = MockServerWebExchange.from(this.request); - this.csrfTokenRepository.setCookieCustomizer((customizer) -> customizer.secure(false)); - this.csrfTokenRepository.saveToken(exchange, createToken()).block(); - ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName); - assertThat(cookie).isNotNull(); - assertThat(cookie.isSecure()).isFalse(); - } - - @Test - void saveTokenWhenSecureFlagFalseAndSslInfoThenNotSecure() { + public void saveTokenWhenSecureFlagFalseAndSslInfoThenNotSecure() { MockServerWebExchange exchange = MockServerWebExchange.from(this.request); this.request.sslInfo(new MockSslInfo()); this.csrfTokenRepository.setSecure(false); @@ -248,23 +182,12 @@ class CookieServerCsrfTokenRepositoryTests { } @Test - void saveTokenWhenSecureFlagFalseAndSslInfoThenNotSecureUsingCustomizer() { - MockServerWebExchange exchange = MockServerWebExchange.from(this.request); - this.request.sslInfo(new MockSslInfo()); - this.csrfTokenRepository.setCookieCustomizer((customizer) -> customizer.secure(false)); - this.csrfTokenRepository.saveToken(exchange, createToken()).block(); - ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName); - assertThat(cookie).isNotNull(); - assertThat(cookie.isSecure()).isFalse(); - } - - @Test - void loadTokenWhenCookieExistThenTokenFound() { + public void loadTokenWhenCookieExistThenTokenFound() { loadAndAssertExpectedValues(); } @Test - void loadTokenWhenCustomThenTokenFound() { + public void loadTokenWhenCustomThenTokenFound() { setExpectedParameterName("paramName"); setExpectedHeaderName("headerName"); setExpectedCookieName("csrfCookie"); @@ -272,20 +195,20 @@ class CookieServerCsrfTokenRepositoryTests { } @Test - void loadTokenWhenNoCookiesThenNullToken() { + public void loadTokenWhenNoCookiesThenNullToken() { MockServerWebExchange exchange = MockServerWebExchange.from(this.request); CsrfToken csrfToken = this.csrfTokenRepository.loadToken(exchange).block(); assertThat(csrfToken).isNull(); } @Test - void loadTokenWhenCookieExistsWithNoValue() { + public void loadTokenWhenCookieExistsWithNoValue() { setExpectedCookieValue(""); loadAndAssertExpectedValues(); } @Test - void loadTokenWhenCookieExistsWithNullValue() { + public void loadTokenWhenCookieExistsWithNullValue() { setExpectedCookieValue(null); loadAndAssertExpectedValues(); } @@ -325,11 +248,6 @@ class CookieServerCsrfTokenRepositoryTests { this.expectedMaxAge = Duration.ofSeconds(expectedCookieMaxAge); } - private void setExpectedSameSitePolicy(String sameSitePolicy) { - this.csrfTokenRepository.setCookieCustomizer((customizer) -> customizer.sameSite(sameSitePolicy)); - this.expectedSameSitePolicy = sameSitePolicy; - } - private void setExpectedCookieValue(String expectedCookieValue) { this.expectedCookieValue = expectedCookieValue; } @@ -366,7 +284,6 @@ class CookieServerCsrfTokenRepositoryTests { assertThat(cookie.isHttpOnly()).isEqualTo(this.expectedHttpOnly); assertThat(cookie.getName()).isEqualTo(this.expectedCookieName); assertThat(cookie.getValue()).isEqualTo(this.expectedCookieValue); - assertThat(cookie.getSameSite()).isEqualTo(this.expectedSameSitePolicy); } private void generateTokenAndAssertExpectedValues() { diff --git a/web/src/test/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandlerTests.java b/web/src/test/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandlerTests.java index 08b54b01f9..c6b800af06 100644 --- a/web/src/test/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandlerTests.java +++ b/web/src/test/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -188,18 +188,6 @@ public class XorServerCsrfTokenRequestAttributeHandlerTests { StepVerifier.create(csrfToken).expectNext(this.token.getToken()).verifyComplete(); } - @Test - public void resolveCsrfTokenIsInvalidThenReturnsNull() { - this.exchange = MockServerWebExchange - .builder(MockServerHttpRequest.post("/") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .body(this.token.getParameterName() + "=" + XOR_CSRF_TOKEN_VALUE)) - .build(); - CsrfToken token = new DefaultCsrfToken("headerName", "paramName", "a"); - Mono csrfToken = this.handler.resolveCsrfTokenValue(this.exchange, token); - assertThat(csrfToken.block()).isNull(); - } - private static Answer fillByteArray() { return (invocation) -> { byte[] bytes = invocation.getArgument(0); diff --git a/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java b/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java index 0c4ce943e0..ac4b785bac 100644 --- a/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java +++ b/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java @@ -46,7 +46,7 @@ public class CookieServerRequestCacheTests { .from(MockServerHttpRequest.get("/secured/").accept(MediaType.TEXT_HTML)); this.cache.saveRequest(exchange).block(); MultiValueMap cookies = exchange.getResponse().getCookies(); - assertThat(cookies).hasSize(1); + assertThat(cookies.size()).isEqualTo(1); ResponseCookie cookie = cookies.getFirst("REDIRECT_URI"); assertThat(cookie).isNotNull(); String encodedRedirectUrl = Base64.getEncoder().encodeToString("/secured/".getBytes()); @@ -60,7 +60,7 @@ public class CookieServerRequestCacheTests { .from(MockServerHttpRequest.get("/secured/").queryParam("key", "value").accept(MediaType.TEXT_HTML)); this.cache.saveRequest(exchange).block(); MultiValueMap cookies = exchange.getResponse().getCookies(); - assertThat(cookies).hasSize(1); + assertThat(cookies.size()).isEqualTo(1); ResponseCookie cookie = cookies.getFirst("REDIRECT_URI"); assertThat(cookie).isNotNull(); String encodedRedirectUrl = Base64.getEncoder().encodeToString("/secured/?key=value".getBytes()); diff --git a/web/src/test/java/org/springframework/security/web/session/SessionManagementFilterTests.java b/web/src/test/java/org/springframework/security/web/session/SessionManagementFilterTests.java index bbd095631f..043bfa1e07 100644 --- a/web/src/test/java/org/springframework/security/web/session/SessionManagementFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/session/SessionManagementFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -20,10 +20,8 @@ import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -31,7 +29,6 @@ import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.session.SessionAuthenticationException; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; @@ -49,11 +46,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; /** * @author Luke Taylor * @author Rob Winch - * @author Mark Chesney */ public class SessionManagementFilterTests { - @BeforeEach @AfterEach public void clearContext() { SecurityContextHolder.clearContext(); @@ -179,69 +174,6 @@ public class SessionManagementFilterTests { assertThat(response.getRedirectedUrl()).isEqualTo("/requested"); } - @Test - public void responseIsRedirectedToRequestedUrlIfContextPathIsSetAndSessionIsInvalid() throws Exception { - // given - DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); - redirectStrategy.setContextRelative(true); - RequestedUrlRedirectInvalidSessionStrategy invalidSessionStrategy = new RequestedUrlRedirectInvalidSessionStrategy(); - invalidSessionStrategy.setCreateNewSession(true); - invalidSessionStrategy.setRedirectStrategy(redirectStrategy); - SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class); - SessionAuthenticationStrategy sessionAuthenticationStrategy = mock(SessionAuthenticationStrategy.class); - SessionManagementFilter filter = new SessionManagementFilter(securityContextRepository, - sessionAuthenticationStrategy); - filter.setInvalidSessionStrategy(invalidSessionStrategy); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setContextPath("/context"); - request.setRequestedSessionId("xxx"); - request.setRequestedSessionIdValid(false); - request.setRequestURI("/context/requested"); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain chain = mock(FilterChain.class); - - // when - filter.doFilter(request, response, chain); - - // then - verify(securityContextRepository).containsContext(request); - verifyNoMoreInteractions(securityContextRepository, sessionAuthenticationStrategy, chain); - assertThat(response.isCommitted()).isTrue(); - assertThat(response.getRedirectedUrl()).isEqualTo("/context/requested"); - assertThat(response.getStatus()).isEqualTo(302); - } - - @Test - public void responseIsRedirectedToRequestedUrlIfStatusCodeIsSetAndSessionIsInvalid() throws Exception { - // given - DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); - redirectStrategy.setStatusCode(HttpStatus.TEMPORARY_REDIRECT); - RequestedUrlRedirectInvalidSessionStrategy invalidSessionStrategy = new RequestedUrlRedirectInvalidSessionStrategy(); - invalidSessionStrategy.setCreateNewSession(true); - invalidSessionStrategy.setRedirectStrategy(redirectStrategy); - SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class); - SessionAuthenticationStrategy sessionAuthenticationStrategy = mock(SessionAuthenticationStrategy.class); - SessionManagementFilter filter = new SessionManagementFilter(securityContextRepository, - sessionAuthenticationStrategy); - filter.setInvalidSessionStrategy(invalidSessionStrategy); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRequestedSessionId("xxx"); - request.setRequestedSessionIdValid(false); - request.setRequestURI("/requested"); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain chain = mock(FilterChain.class); - - // when - filter.doFilter(request, response, chain); - - // then - verify(securityContextRepository).containsContext(request); - verifyNoMoreInteractions(securityContextRepository, sessionAuthenticationStrategy, chain); - assertThat(response.isCommitted()).isTrue(); - assertThat(response.getRedirectedUrl()).isEqualTo("/requested"); - assertThat(response.getStatus()).isEqualTo(307); - } - @Test public void customAuthenticationTrustResolver() throws Exception { AuthenticationTrustResolver trustResolver = mock(AuthenticationTrustResolver.class); diff --git a/web/src/test/java/org/springframework/security/web/util/TextEscapeUtilsTests.java b/web/src/test/java/org/springframework/security/web/util/TextEscapeUtilsTests.java index 2a8cc8235e..71482298c2 100644 --- a/web/src/test/java/org/springframework/security/web/util/TextEscapeUtilsTests.java +++ b/web/src/test/java/org/springframework/security/web/util/TextEscapeUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -62,7 +62,7 @@ public class TextEscapeUtilsTests { @Test public void undefinedSurrogatePairIsIgnored() { - assertThat(TextEscapeUtils.escapeEntities("abc\uDBFF\uDFFFa")).isEqualTo("abca"); + assertThat(TextEscapeUtils.escapeEntities("abc\uD888\uDC00a")).isEqualTo("abca"); } } diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java index 62b3781790..ecf12ba614 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -19,7 +19,6 @@ package org.springframework.security.web.util.matcher; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Test; @@ -27,8 +26,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNullPointerException; @@ -126,30 +123,4 @@ public class AndRequestMatcherTests { assertThat(this.matcher.matches(this.request)).isFalse(); } - @Test - public void matcherWhenMatchersHavePlaceholdersThenPropagatesMatches() { - this.matcher = new AndRequestMatcher(this.delegate, this.delegate2); - - given(this.delegate.matcher(this.request)).willReturn(MatchResult.match(Map.of("param", "value"))); - given(this.delegate2.matcher(this.request)).willReturn(MatchResult.match(Map.of("param", "othervalue"))); - MatchResult result = this.matcher.matcher(this.request); - assertThat(result.getVariables()).containsExactlyEntriesOf(Map.of("param", "othervalue")); - - given(this.delegate.matcher(this.request)).willReturn(MatchResult.match()); - given(this.delegate2.matcher(this.request)).willReturn(MatchResult.match(Map.of("param", "value"))); - result = this.matcher.matcher(this.request); - assertThat(result.getVariables()).containsExactlyEntriesOf(Map.of("param", "value")); - - given(this.delegate.matcher(this.request)).willReturn(MatchResult.match(Map.of("param", "value"))); - given(this.delegate2.matcher(this.request)).willReturn(MatchResult.notMatch()); - result = this.matcher.matcher(this.request); - assertThat(result.getVariables()).isEmpty(); - - given(this.delegate.matcher(this.request)).willReturn(MatchResult.match(Map.of("otherparam", "value"))); - given(this.delegate2.matcher(this.request)).willReturn(MatchResult.match(Map.of("param", "value"))); - result = this.matcher.matcher(this.request); - assertThat(result.getVariables()) - .containsExactlyInAnyOrderEntriesOf(Map.of("otherparam", "value", "param", "value")); - } - } diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/MediaTypeRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/MediaTypeRequestMatcherTests.java index a7adcabb8f..6267e81e32 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/MediaTypeRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/MediaTypeRequestMatcherTests.java @@ -88,7 +88,7 @@ public class MediaTypeRequestMatcherTests { @Test public void constructorWhenEmptyMediaTypeThenIAE() { - assertThatIllegalArgumentException().isThrownBy(MediaTypeRequestMatcher::new); + assertThatIllegalArgumentException().isThrownBy(() -> new MediaTypeRequestMatcher()); } @Test diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java index 72dd4162bd..831e3f56d3 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -19,7 +19,6 @@ package org.springframework.security.web.util.matcher; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Test; @@ -27,13 +26,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verifyNoInteractions; /** * @author Rob Winch @@ -126,26 +122,4 @@ public class OrRequestMatcherTests { assertThat(this.matcher.matches(this.request)).isTrue(); } - @Test - public void matcherWhenMatchersHavePlaceholdersThenPropagatesFirstMatch() { - this.matcher = new OrRequestMatcher(this.delegate, this.delegate2); - - given(this.delegate.matcher(this.request)).willReturn(MatchResult.match(Map.of("param", "value"))); - given(this.delegate2.matcher(this.request)).willReturn(MatchResult.match(Map.of("param", "othervalue"))); - MatchResult result = this.matcher.matcher(this.request); - assertThat(result.getVariables()).containsExactlyEntriesOf(Map.of("param", "value")); - verifyNoInteractions(this.delegate2); - - given(this.delegate.matcher(this.request)).willReturn(MatchResult.match()); - given(this.delegate2.matcher(this.request)).willReturn(MatchResult.match(Map.of("param", "value"))); - result = this.matcher.matcher(this.request); - assertThat(result.getVariables()).isEmpty(); - verifyNoInteractions(this.delegate2); - - given(this.delegate.matcher(this.request)).willReturn(MatchResult.notMatch()); - given(this.delegate2.matcher(this.request)).willReturn(MatchResult.match(Map.of("param", "value"))); - result = this.matcher.matcher(this.request); - assertThat(result.getVariables()).containsExactlyEntriesOf(Map.of("param", "value")); - } - } diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/RequestMatchersTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/RequestMatchersTests.java deleted file mode 100644 index abff0c345c..0000000000 --- a/web/src/test/java/org/springframework/security/web/util/matcher/RequestMatchersTests.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2002-2023 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.web.util.matcher; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link RequestMatchers}. - * - * @author Christian Schuster - */ -class RequestMatchersTests { - - @Test - void checkAnyOfWhenOneMatchThenMatch() { - RequestMatcher composed = RequestMatchers.anyOf((r) -> false, (r) -> true); - boolean match = composed.matches(null); - assertThat(match).isTrue(); - } - - @Test - void checkAnyOfWhenNoneMatchThenNotMatch() { - RequestMatcher composed = RequestMatchers.anyOf((r) -> false, (r) -> false); - boolean match = composed.matches(null); - assertThat(match).isFalse(); - } - - @Test - void checkAnyOfWhenEmptyThenNotMatch() { - RequestMatcher composed = RequestMatchers.anyOf(); - boolean match = composed.matches(null); - assertThat(match).isFalse(); - } - - @Test - void checkAllOfWhenOneNotMatchThenNotMatch() { - RequestMatcher composed = RequestMatchers.allOf((r) -> false, (r) -> true); - boolean match = composed.matches(null); - assertThat(match).isFalse(); - } - - @Test - void checkAllOfWhenAllMatchThenMatch() { - RequestMatcher composed = RequestMatchers.allOf((r) -> true, (r) -> true); - boolean match = composed.matches(null); - assertThat(match).isTrue(); - } - - @Test - void checkAllOfWhenEmptyThenMatch() { - RequestMatcher composed = RequestMatchers.allOf(); - boolean match = composed.matches(null); - assertThat(match).isTrue(); - } - - @Test - void checkNotWhenMatchThenNotMatch() { - RequestMatcher composed = RequestMatchers.not((r) -> true); - boolean match = composed.matches(null); - assertThat(match).isFalse(); - } - - @Test - void checkNotWhenNotMatchThenMatch() { - RequestMatcher composed = RequestMatchers.not((r) -> false); - boolean match = composed.matches(null); - assertThat(match).isTrue(); - } - -} diff --git a/web/src/test/resources/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPointTest-context.xml b/web/src/test/resources/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPointTest-context.xml index 508daae44c..66d965955b 100644 --- a/web/src/test/resources/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPointTest-context.xml +++ b/web/src/test/resources/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPointTest-context.xml @@ -11,15 +11,15 @@ - + - + - - + + - + - +