diff --git a/Jenkinsfile b/Jenkinsfile index 8703b270ffc..b4a7554c829 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -39,27 +39,12 @@ try { } } }, - springio: { - stage('Spring IO') { - node { - checkout scm - try { - sh "./gradlew clean springIoCheck -PplatformVersion=Cairo-BUILD-SNAPSHOT -PexcludeProjects='**/samples/**' --refresh-dependencies --no-daemon --stacktrace" - } catch(Exception e) { - currentBuild.result = 'FAILED: springio' - throw e - } finally { - junit '**/build/spring-io*-results/*.xml' - } - } - } - }, snapshots: { stage('Snapshot Tests') { node { checkout scm try { - sh "./gradlew clean test -PspringVersion='5.+' -PreactorVersion=Bismuth-BUILD-SNAPSHOT -PspringDataVersion=Kay-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace" + sh "./gradlew clean test -PspringVersion='5.+' -PreactorVersion=Californium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT --refresh-dependencies --no-daemon --stacktrace" } catch(Exception e) { currentBuild.result = 'FAILED: snapshots' throw e @@ -73,7 +58,7 @@ try { checkout scm try { withEnv(["JAVA_HOME=${ tool 'jdk9' }"]) { - sh "./gradlew clean test --no-daemon --stacktrace" + sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace" } } catch(Exception e) { currentBuild.result = 'FAILED: jdk9' @@ -88,7 +73,7 @@ try { checkout scm try { withEnv(["JAVA_HOME=${ tool 'jdk10' }"]) { - sh "./gradlew clean test --no-daemon --stacktrace" + sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace" } } catch(Exception e) { currentBuild.result = 'FAILED: jdk10' @@ -103,7 +88,7 @@ try { checkout scm try { withEnv(["JAVA_HOME=${ tool 'jdk11' }"]) { - sh "./gradlew clean test --no-daemon --stacktrace" + sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace" } } catch(Exception e) { currentBuild.result = 'FAILED: jdk11' diff --git a/acl/src/test/java/org/springframework/security/acls/jdbc/EhCacheBasedAclCacheTests.java b/acl/src/test/java/org/springframework/security/acls/jdbc/EhCacheBasedAclCacheTests.java index cb97097677f..365e882c498 100644 --- a/acl/src/test/java/org/springframework/security/acls/jdbc/EhCacheBasedAclCacheTests.java +++ b/acl/src/test/java/org/springframework/security/acls/jdbc/EhCacheBasedAclCacheTests.java @@ -154,11 +154,11 @@ public void testDiskSerializationOfMutableAclObjectInstance() throws Exception { Object retrieved1 = FieldUtils.getProtectedFieldValue("aclAuthorizationStrategy", retrieved); - assertThat(retrieved1).isEqualTo(null); + assertThat(retrieved1).isNull(); Object retrieved2 = FieldUtils.getProtectedFieldValue( "permissionGrantingStrategy", retrieved); - assertThat(retrieved2).isEqualTo(null); + assertThat(retrieved2).isNull(); } @Test diff --git a/build.gradle b/build.gradle index 53de5b415a0..40fed3c1e7b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { dependencies { - classpath 'io.spring.gradle:spring-build-conventions:0.0.15.RELEASE' + classpath 'io.spring.gradle:spring-build-conventions:0.0.16.RELEASE' classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" } repositories { 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 index 389191fb2ce..c45cf91e438 100644 --- a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java +++ b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java @@ -353,7 +353,7 @@ public void ignoresClassesItDoesNotSupport() throws Exception { assertThat(cap.supports(TestingAuthenticationToken.class)).isFalse(); // Try it anyway - assertThat(cap.authenticate(token)).isEqualTo(null); + assertThat(cap.authenticate(token)).isNull(); } @Test @@ -370,7 +370,7 @@ public void ignoresUsernamePasswordAuthenticationTokensWithoutCasIdentifiersAsPr UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "some_normal_user", "password", AuthorityUtils.createAuthorityList("ROLE_A")); - assertThat(cap.authenticate(token)).isEqualTo(null); + assertThat(cap.authenticate(token)).isNull(); } @Test diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 23fa96f840a..4891d67cc4e 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -13,6 +13,7 @@ dependencies { optional project(':spring-security-messaging') optional project(':spring-security-oauth2-client') optional project(':spring-security-oauth2-jose') + optional project(':spring-security-oauth2-resource-server') optional project(':spring-security-openid') optional project(':spring-security-web') optional 'io.projectreactor:reactor-core' @@ -34,7 +35,9 @@ dependencies { testCompile apachedsDependencies testCompile powerMock2Dependencies testCompile spockDependencies + testCompile 'com.squareup.okhttp3:mockwebserver' testCompile 'ch.qos.logback:logback-classic' + testCompile 'io.projectreactor.netty:reactor-netty' testCompile 'javax.annotation:jsr250-api:1.0' testCompile 'javax.xml.bind:jaxb-api' testCompile 'ldapsdk:ldapsdk:4.1' 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 29d64aee426..1e71774a867 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 @@ -122,7 +122,7 @@ public O getOrBuild() { * invokes {@link SecurityConfigurerAdapter#setBuilder(SecurityBuilder)}. * * @param configurer - * @return + * @return the {@link SecurityConfigurerAdapter} for further customizations * @throws Exception */ @SuppressWarnings("unchecked") @@ -140,7 +140,7 @@ public > C apply(C configurer) * are not considered. * * @param configurer - * @return + * @return the {@link SecurityConfigurerAdapter} for further customizations * @throws Exception */ public > C apply(C configurer) throws Exception { @@ -172,7 +172,7 @@ public C getSharedObject(Class sharedType) { /** * Gets the shared objects - * @return + * @return the shared Objects */ public Map, Object> getSharedObjects() { return Collections.unmodifiableMap(this.sharedObjects); @@ -214,7 +214,7 @@ private > void add(C configurer) throws Excep * List if not found. Note that object hierarchies are not considered. * * @param clazz the {@link SecurityConfigurer} class to look for - * @return + * @return a list of {@link SecurityConfigurer}s for further customization */ @SuppressWarnings("unchecked") public > List getConfigurers(Class clazz) { @@ -230,7 +230,7 @@ public > List getConfigurers(Class claz * List if not found. Note that object hierarchies are not considered. * * @param clazz the {@link SecurityConfigurer} class to look for - * @return + * @return a list of {@link SecurityConfigurer}s for further customization */ @SuppressWarnings("unchecked") public > List removeConfigurers(Class clazz) { @@ -246,7 +246,7 @@ public > List removeConfigurers(Class c * found. Note that object hierarchies are not considered. * * @param clazz - * @return + * @return the {@link SecurityConfigurer} for further customizations */ @SuppressWarnings("unchecked") public > C getConfigurer(Class clazz) { @@ -359,7 +359,7 @@ protected void beforeConfigure() throws Exception { /** * Subclasses must implement this method to build the object that is being returned. * - * @return + * @return the Object to be buit or null if the implementation allows it */ protected abstract O performBuild() throws Exception; 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 b36d406a402..9e519149ed4 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 @@ -51,7 +51,7 @@ public void configure(B builder) throws Exception { * Return the {@link SecurityBuilder} when done using the {@link SecurityConfigurer}. * This is useful for method chaining. * - * @return + * @return the {@link SecurityBuilder} for further customizations */ public B and() { return getBuilder(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java index 52f23fd3eb6..aaffc81009f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java @@ -258,7 +258,7 @@ protected ProviderManager performBuild() throws Exception { * default configuration in the {@link SecurityConfigurer#configure(SecurityBuilder)} * method. * - * @return + * @return true, if {@link AuthenticationManagerBuilder} is configured, otherwise false */ public boolean isConfigured() { return !authenticationProviders.isEmpty() || parentAuthenticationManager != null; diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java index 9ac13368577..11dc6c4c7c1 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java @@ -289,6 +289,11 @@ public boolean matches(CharSequence rawPassword, return getPasswordEncoder().matches(rawPassword, encodedPassword); } + @Override + public boolean upgradeEncoding(String encodedPassword) { + return getPasswordEncoder().upgradeEncoding(encodedPassword); + } + private PasswordEncoder getPasswordEncoder() { if (this.passwordEncoder != null) { return this.passwordEncoder; diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java index 54e9ac4d0c5..db4d2968dd9 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java @@ -22,6 +22,7 @@ import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; /** * Lazily initializes the global authentication with a {@link UserDetailsService} if it is @@ -65,12 +66,16 @@ public void configure(AuthenticationManagerBuilder auth) throws Exception { } PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); + UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); if (passwordEncoder != null) { provider.setPasswordEncoder(passwordEncoder); } + if (passwordManager != null) { + provider.setUserDetailsPasswordService(passwordManager); + } provider.afterPropertiesSet(); auth.authenticationProvider(provider); @@ -90,4 +95,4 @@ private T getBeanOrNull(Class type) { .getBean(userDetailsBeanNames[0], type); } } -} \ No newline at end of file +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java index 4b9725abdb1..fde27dad55f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java @@ -302,7 +302,7 @@ public LdapAuthenticationProviderConfigurer userDetailsContextMapper( /** * Specifies the attribute name which contains the role name. Default is "cn". * @param groupRoleAttribute the attribute name that maps a group to a role. - * @return + * @return the {@link LdapAuthenticationProviderConfigurer} for further customizations */ public LdapAuthenticationProviderConfigurer groupRoleAttribute( String groupRoleAttribute) { @@ -384,11 +384,11 @@ public void configure(B builder) throws Exception { */ public final class PasswordCompareConfigurer { - /** + /**Us * Allows specifying the {@link PasswordEncoder} to use. The default is * {@link org.springframework.security.crypto.password.NoOpPasswordEncoder}. * @param passwordEncoder the {@link PasswordEncoder} to use - * @return the {@link PasswordEncoder} to use + * @return the {@link PasswordCompareConfigurer} for further customizations */ public PasswordCompareConfigurer passwordEncoder(PasswordEncoder passwordEncoder) { LdapAuthenticationProviderConfigurer.this.passwordEncoder = passwordEncoder; @@ -602,7 +602,7 @@ private BaseLdapPathContextSource getContextSource() throws Exception { } /** - * @return + * @return the {@link PasswordCompareConfigurer} for further customizations */ public PasswordCompareConfigurer passwordCompare() { return new PasswordCompareConfigurer().passwordAttribute("password") diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserDetailsManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserDetailsManagerConfigurer.java index 3f5ea1f9695..7d2ae974023 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserDetailsManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserDetailsManagerConfigurer.java @@ -63,7 +63,7 @@ public JdbcUserDetailsManagerConfigurer() { * Populates the {@link DataSource} to be used. This is the only required attribute. * * @param dataSource the {@link DataSource} to be used. Cannot be null. - * @return + * @return The {@link JdbcUserDetailsManagerConfigurer} used for additional customizations * @throws Exception */ public JdbcUserDetailsManagerConfigurer dataSource(DataSource dataSource) @@ -142,7 +142,7 @@ public JdbcUserDetailsManagerConfigurer groupAuthoritiesByUsername(String que * storage (default is ""). * * @param rolePrefix - * @return + * @return The {@link JdbcUserDetailsManagerConfigurer} used for additional customizations * @throws Exception */ public JdbcUserDetailsManagerConfigurer rolePrefix(String rolePrefix) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurer.java index 6dfd328c951..961a0b3cf96 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/UserDetailsManagerConfigurer.java @@ -69,7 +69,7 @@ protected void initUserDetailsService() throws Exception { * method can be invoked multiple times to add multiple users. * * @param userDetails the user to add. Cannot be null. - * @return + * @return the {@link UserDetailsBuilder} for further customizations */ @SuppressWarnings("unchecked") public final C withUser(UserDetails userDetails) { @@ -82,7 +82,7 @@ public final C withUser(UserDetails userDetails) { * method can be invoked multiple times to add multiple users. * * @param userBuilder the user to add. Cannot be null. - * @return + * @return the {@link UserDetailsBuilder} for further customizations */ @SuppressWarnings("unchecked") public final C withUser(User.UserBuilder userBuilder) { @@ -95,7 +95,7 @@ public final C withUser(User.UserBuilder userBuilder) { * method can be invoked multiple times to add multiple users. * * @param username the username for the user being added. Cannot be null. - * @return + * @return the {@link UserDetailsBuilder} for further customizations */ @SuppressWarnings("unchecked") public final UserDetailsBuilder withUser(String username) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java index 54133e6a304..e58f6ce06ab 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java @@ -21,6 +21,7 @@ import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; /** * Allows configuring a {@link DaoAuthenticationProvider} @@ -46,6 +47,9 @@ abstract class AbstractDaoAuthenticationConfigurer objectPostProcessor) { * {@link DaoAuthenticationProvider}. The default is to use plain text. * * @param passwordEncoder The {@link PasswordEncoder} to use. - * @return + * @return the {@link AbstractDaoAuthenticationConfigurer} for further customizations */ @SuppressWarnings("unchecked") public C passwordEncoder(PasswordEncoder passwordEncoder) { @@ -73,6 +77,11 @@ public C passwordEncoder(PasswordEncoder passwordEncoder) { return (C) this; } + public C userDetailsPasswordManager(UserDetailsPasswordService passwordManager) { + provider.setUserDetailsPasswordService(passwordManager); + return (C) this; + } + @Override public void configure(B builder) throws Exception { provider = postProcess(provider); diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java index a2aa8ac794a..83e0e8e35c3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java @@ -118,7 +118,7 @@ public T postProcess(T object) { * {@link MethodInterceptor}. *

* - * @return + * @return the {@link MethodInterceptor}. * @throws Exception */ @Bean @@ -204,7 +204,7 @@ private void initializeMethodSecurityInterceptor() throws Exception { * {@link AfterInvocationManager} *

* - * @return + * @return the {@link AfterInvocationManager} to use */ protected AfterInvocationManager afterInvocationManager() { if (prePostEnabled()) { @@ -225,7 +225,7 @@ protected AfterInvocationManager afterInvocationManager() { * Provide a custom {@link RunAsManager} for the default implementation of * {@link #methodSecurityInterceptor()}. The default is null. * - * @return + * @return the {@link RunAsManager} to use */ protected RunAsManager runAsManager() { return null; @@ -241,7 +241,7 @@ protected RunAsManager runAsManager() { *
  • {@link AuthenticatedVoter}
  • * * - * @return + * @return the {@link AccessDecisionManager} to use */ protected AccessDecisionManager accessDecisionManager() { List> decisionVoters = new ArrayList>(); @@ -270,7 +270,7 @@ protected AccessDecisionManager accessDecisionManager() { * {@link MethodSecurityExpressionHandler} *

    * - * @return + * @return the {@link MethodSecurityExpressionHandler} to use */ protected MethodSecurityExpressionHandler createExpressionHandler() { return defaultMethodExpressionHandler; @@ -307,7 +307,7 @@ protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() { * {@link #configure(AuthenticationManagerBuilder)} was not overridden, then an * {@link AuthenticationManager} is attempted to be autowired by type. * - * @return + * @return the {@link AuthenticationManager} to use */ protected AuthenticationManager authenticationManager() throws Exception { if (authenticationManager == null) { @@ -346,7 +346,7 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { * {@link #customMethodSecurityMetadataSource()} and the attributes on * {@link EnableGlobalMethodSecurity}. * - * @return + * @return the {@link MethodSecurityMetadataSource} */ @Bean public MethodSecurityMetadataSource methodSecurityMetadataSource() { @@ -379,7 +379,7 @@ public MethodSecurityMetadataSource methodSecurityMetadataSource() { * Creates the {@link PreInvocationAuthorizationAdvice} to be used. The default is * {@link ExpressionBasedPreInvocationAdvice}. * - * @return + * @return the {@link PreInvocationAuthorizationAdvice} */ @Bean public PreInvocationAuthorizationAdvice preInvocationAuthorizationAdvice() { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java index f772c7d62f3..bdd1fe911e6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java @@ -146,8 +146,10 @@ > C removeConfigurer *
  • {@link ConcurrentSessionFilter}
  • *
  • {@link OpenIDAuthenticationFilter}
  • *
  • {@link org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter}
  • + *
  • {@link org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter}
  • *
  • {@link ConcurrentSessionFilter}
  • *
  • {@link DigestAuthenticationFilter}
  • + *
  • {@link org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter}
  • *
  • {@link BasicAuthenticationFilter}
  • *
  • {@link RequestCacheAwareFilter}
  • *
  • {@link SecurityContextHolderAwareRequestFilter}
  • diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java index 20f336d6091..7c1fb68c1f0 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java @@ -19,7 +19,6 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Map; - import javax.servlet.Filter; import org.springframework.security.web.access.ExceptionTranslationFilter; @@ -33,6 +32,7 @@ import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter; import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.authentication.www.DigestAuthenticationFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; @@ -101,10 +101,15 @@ final class FilterComparator implements Comparator, Serializable { order += STEP; put(DefaultLoginPageGeneratingFilter.class, order); order += STEP; + put(DefaultLogoutPageGeneratingFilter.class, order); + order += STEP; put(ConcurrentSessionFilter.class, order); order += STEP; put(DigestAuthenticationFilter.class, order); order += STEP; + filterToOrder.put( + "org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter", order); + order += STEP; put(BasicAuthenticationFilter.class, order); order += STEP; put(RequestCacheAwareFilter.class, order); 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 da2416d4b78..1d41bdd3fbe 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 @@ -327,7 +327,7 @@ public OpenIDLoginConfigurer openidLogin() throws Exception { * } * * - * @return + * @return the {@link HeadersConfigurer} for further customizations * @throws Exception * @see HeadersConfigurer */ @@ -643,7 +643,7 @@ public RememberMeConfigurer rememberMe() throws Exception { * * @see #requestMatcher(RequestMatcher) * - * @return + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customizations * @throws Exception */ public ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry authorizeRequests() @@ -763,7 +763,7 @@ public CsrfConfigurer csrf() throws Exception { * } * * - * @return + * @return the {@link LogoutConfigurer} for further customizations * @throws Exception */ public LogoutConfigurer logout() throws Exception { @@ -826,7 +826,7 @@ public LogoutConfigurer logout() throws Exception { * } * * - * @return + * @return the {@link AnonymousConfigurer} for further customizations * @throws Exception */ public AnonymousConfigurer anonymous() throws Exception { @@ -890,7 +890,7 @@ public AnonymousConfigurer anonymous() throws Exception { * * @see FormLoginConfigurer#loginPage(String) * - * @return + * @return the {@link FormLoginConfigurer} for further customizations * @throws Exception */ public FormLoginConfigurer formLogin() throws Exception { @@ -999,8 +999,10 @@ public OAuth2LoginConfigurer oauth2Login() throws Exception { * @return the {@link OAuth2Configurer} for further customizations * @throws Exception */ - public OAuth2Configurer oauth2() throws Exception { - return getOrApply(new OAuth2Configurer()); + public OAuth2Configurer oauth2() throws Exception { + OAuth2Configurer configurer = getOrApply(new OAuth2Configurer<>()); + this.postProcess(configurer); + return configurer; } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index 4f9d94f3ac0..85e6c1fb503 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -235,7 +235,7 @@ public WebSecurity expressionHandler( /** * Gets the {@link SecurityExpressionHandler} to be used. - * @return + * @return the {@link SecurityExpressionHandler} for further customizations */ public SecurityExpressionHandler getExpressionHandler() { return expressionHandler; @@ -243,7 +243,7 @@ public SecurityExpressionHandler getExpressionHandler() { /** * Gets the {@link WebInvocationPrivilegeEvaluator} to be used. - * @return + * @return the {@link WebInvocationPrivilegeEvaluator} for further customizations */ public WebInvocationPrivilegeEvaluator getPrivilegeEvaluator() { if (privilegeEvaluator != null) { 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 940d685eafe..8fc51dc26cc 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 @@ -52,7 +52,7 @@ * } * * @Override - * protected void configure(AuthenticationManagerBuilder auth) { + * protected void configure(AuthenticationManagerBuilder auth) throws Exception { * auth * // enable in memory based authentication with a user named "user" and "admin" * .inMemoryAuthentication().withUser("user").password("password").roles("USER") 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 e0ff29b5992..baa697a0098 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 @@ -20,9 +20,9 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.method.annotation.OAuth2ClientArgumentResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.method.annotation.OAuth2AuthorizedClientArgumentResolver; import org.springframework.util.ClassUtils; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -58,18 +58,30 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { @Configuration static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer { - @Autowired(required = false) private ClientRegistrationRepository clientRegistrationRepository; - - @Autowired(required = false) - private OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository authorizedClientRepository; @Override public void addArgumentResolvers(List argumentResolvers) { - if (this.clientRegistrationRepository != null && this.authorizedClientService != null) { - OAuth2ClientArgumentResolver oauth2ClientArgumentResolver = new OAuth2ClientArgumentResolver( - this.clientRegistrationRepository, this.authorizedClientService); - argumentResolvers.add(oauth2ClientArgumentResolver); + if (this.clientRegistrationRepository != null && this.authorizedClientRepository != null) { + OAuth2AuthorizedClientArgumentResolver authorizedClientArgumentResolver = + new OAuth2AuthorizedClientArgumentResolver( + this.clientRegistrationRepository, this.authorizedClientRepository); + argumentResolvers.add(authorizedClientArgumentResolver); + } + } + + @Autowired(required = false) + public void setClientRegistrationRepository(List clientRegistrationRepositories) { + if (clientRegistrationRepositories.size() == 1) { + this.clientRegistrationRepository = clientRegistrationRepositories.get(0); + } + } + + @Autowired(required = false) + public void setAuthorizedClientRepository(List authorizedClientRepositories) { + if (authorizedClientRepositories.size() == 1) { + this.authorizedClientRepository = authorizedClientRepositories.get(0); } } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java index f3caec0faa7..4e0f8a3f3ae 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java @@ -88,7 +88,7 @@ public SecurityExpressionHandler webSecurityExpressionHandler( /** * Creates the Spring Security Filter Chain - * @return + * @return the {@link Filter} that represents the security filter chain * @throws Exception */ @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java index c75b94ccb5d..0377e53a321 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java @@ -257,7 +257,7 @@ public AuthenticationManager authenticationManagerBean() throws Exception { * {@link AuthenticationManagerBuilder} that was passed in. Otherwise, autowire the * {@link AuthenticationManager} by type. * - * @return + * @return the {@link AuthenticationManager} to use * @throws Exception */ protected AuthenticationManager authenticationManager() throws Exception { @@ -291,7 +291,7 @@ protected AuthenticationManager authenticationManager() throws Exception { * * To change the instance returned, developers should change * {@link #userDetailsService()} instead - * @return + * @return the {@link UserDetailsService} * @throws Exception * @see #userDetailsService() */ @@ -308,7 +308,7 @@ public UserDetailsService userDetailsServiceBean() throws Exception { * {@link ApplicationContext}. Developers should override this method when changing * the instance of {@link #userDetailsServiceBean()}. * - * @return + * @return the {@link UserDetailsService} to use */ protected UserDetailsService userDetailsService() { AuthenticationManagerBuilder globalAuthBuilder = context @@ -593,6 +593,11 @@ public boolean matches(CharSequence rawPassword, return getPasswordEncoder().matches(rawPassword, encodedPassword); } + @Override + public boolean upgradeEncoding(String encodedPassword) { + return getPasswordEncoder().upgradeEncoding(encodedPassword); + } + private PasswordEncoder getPasswordEncoder() { if (this.passwordEncoder != null) { return this.passwordEncoder; 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 0bd4c1fc611..c39919867cc 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-2013 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. @@ -32,6 +32,7 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; @@ -65,7 +66,8 @@ public abstract class AbstractAuthenticationFilterConfigurer authenticationDetailsSource; - private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); + private SavedRequestAwareAuthenticationSuccessHandler defaultSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler(); + private AuthenticationSuccessHandler successHandler = this.defaultSuccessHandler; private LoginUrlAuthenticationEntryPoint authenticationEntryPoint; @@ -128,6 +130,7 @@ public final T defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) { SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler(); handler.setDefaultTargetUrl(defaultSuccessUrl); handler.setAlwaysUseDefaultTargetUrl(alwaysUse); + this.defaultSuccessHandler = handler; return successHandler(handler); } @@ -181,7 +184,7 @@ public final T successHandler(AuthenticationSuccessHandler successHandler) { /** * Equivalent of invoking permitAll(true) - * @return + * @return the {@link FormLoginConfigurer} for additional customization */ public final T permitAll() { return permitAll(true); @@ -234,20 +237,27 @@ public final T failureHandler( @Override public void init(B http) throws Exception { updateAuthenticationDefaults(); - if (permitAll) { - PermitAllSupport.permitAll(http, loginPage, loginProcessingUrl, failureUrl); - } - + updateAccessDefaults(http); registerDefaultAuthenticationEntryPoint(http); } @SuppressWarnings("unchecked") - private void registerDefaultAuthenticationEntryPoint(B http) { + protected final void registerDefaultAuthenticationEntryPoint(B http) { + registerAuthenticationEntryPoint(http, this.authenticationEntryPoint); + } + + @SuppressWarnings("unchecked") + protected final void registerAuthenticationEntryPoint(B http, AuthenticationEntryPoint authenticationEntryPoint) { ExceptionHandlingConfigurer exceptionHandling = http .getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling == null) { return; } + exceptionHandling.defaultAuthenticationEntryPointFor( + postProcess(authenticationEntryPoint), getAuthenticationEntryPointMatcher(http)); + } + + protected final RequestMatcher getAuthenticationEntryPointMatcher(B http) { ContentNegotiationStrategy contentNegotiationStrategy = http .getSharedObject(ContentNegotiationStrategy.class); if (contentNegotiationStrategy == null) { @@ -262,10 +272,7 @@ private void registerDefaultAuthenticationEntryPoint(B http) { RequestMatcher notXRequestedWith = new NegatedRequestMatcher( new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); - RequestMatcher preferredMatcher = new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher)); - - exceptionHandling.defaultAuthenticationEntryPointFor( - postProcess(authenticationEntryPoint), preferredMatcher); + return new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher)); } @Override @@ -275,6 +282,11 @@ public void configure(B http) throws Exception { authenticationEntryPoint.setPortMapper(portMapper); } + RequestCache requestCache = http.getSharedObject(RequestCache.class); + if (requestCache != null) { + this.defaultSuccessHandler.setRequestCache(requestCache); + } + authFilter.setAuthenticationManager(http .getSharedObject(AuthenticationManager.class)); authFilter.setAuthenticationSuccessHandler(successHandler); @@ -351,6 +363,15 @@ protected final String getLoginPage() { return loginPage; } + /** + * Gets the Authentication Entry Point + * + * @return the Authentication Entry Point + */ + protected final AuthenticationEntryPoint getAuthenticationEntryPoint() { + return authenticationEntryPoint; + } + /** * Gets the URL to submit an authentication request to (i.e. where username/password * must be submitted) @@ -364,7 +385,7 @@ protected final String getLoginProcessingUrl() { /** * Gets the URL to send users to if authentication fails * - * @return + * @return the URL to send users if authentication fails (e.g. "/login?error"). */ protected final String getFailureUrl() { return failureUrl; @@ -375,7 +396,7 @@ protected final String getFailureUrl() { * * @throws Exception */ - private void updateAuthenticationDefaults() { + protected final void updateAuthenticationDefaults() { if (loginProcessingUrl == null) { loginProcessingUrl(loginPage); } @@ -390,6 +411,15 @@ private void updateAuthenticationDefaults() { } } + /** + * Updates the default values for access. + */ + protected final void updateAccessDefaults(B http) { + if (permitAll) { + PermitAllSupport.permitAll(http, loginPage, loginProcessingUrl, failureUrl); + } + } + /** * Sets the loginPage and updates the {@link AuthenticationEntryPoint}. * @param loginPage 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 db0a23a7ac9..80eb1d2fda9 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 @@ -188,7 +188,7 @@ public ChannelRequestMatcherRegistry withObjectPostProcessor( * Sets the {@link ChannelProcessor} instances to use in * {@link ChannelDecisionManagerImpl} * @param channelProcessors - * @return + * @return the {@link ChannelSecurityConfigurer} for further customizations */ public ChannelRequestMatcherRegistry channelProcessors( List channelProcessors) { @@ -200,7 +200,7 @@ public ChannelRequestMatcherRegistry channelProcessors( * Return the {@link SecurityBuilder} when done using the * {@link SecurityConfigurer}. This is useful for method chaining. * - * @return + * @return the type of {@link HttpSecurityBuilder} that is being configured */ 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 a1143cc3d59..9529703af6e 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-2013 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. @@ -128,7 +128,7 @@ public CsrfConfigurer requireCsrfProtectionMatcher( *

    * *

    - * The following will ensure CSRF protection ignores: + * For example, the following configuration will ensure CSRF protection ignores: *

    *
      *
    • Any GET, HEAD, TRACE, OPTIONS (this is the default)
    • @@ -150,6 +150,35 @@ public CsrfConfigurer ignoringAntMatchers(String... antPatterns) { .and(); } + /** + *

      + * Allows specifying {@link HttpServletRequest}s that should not use CSRF Protection + * even if they match the {@link #requireCsrfProtectionMatcher(RequestMatcher)}. + *

      + * + *

      + * For example, the following configuration will ensure CSRF protection ignores: + *

      + *
        + *
      • Any GET, HEAD, TRACE, OPTIONS (this is the default)
      • + *
      • We also explicitly state to ignore any request that has a "X-Requested-With: XMLHttpRequest" header
      • + *
      + * + *
      +	 * http
      +	 *     .csrf()
      +	 *         .ignoringRequestMatchers(request -> "XMLHttpRequest".equals(request.getHeader("X-Requested-With")))
      +	 *         .and()
      +	 *     ...
      +	 * 
      + * + * @since 5.1 + */ + public CsrfConfigurer ignoringRequestMatchers(RequestMatcher... requestMatchers) { + return new IgnoreCsrfProtectionRegistry(this.context).requestMatchers(requestMatchers) + .and(); + } + @SuppressWarnings("unchecked") @Override public void configure(H http) throws Exception { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java index 5d2fa4930de..84de25722d8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java @@ -19,9 +19,13 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; import org.springframework.security.web.csrf.CsrfToken; +import javax.servlet.http.HttpServletRequest; import java.util.Collections; +import java.util.Map; +import java.util.function.Function; /** * Adds a Filter that will generate a login page if one is not specified otherwise when @@ -66,15 +70,19 @@ public final class DefaultLoginPageConfigurer> private DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = new DefaultLoginPageGeneratingFilter(); + private DefaultLogoutPageGeneratingFilter logoutPageGeneratingFilter = new DefaultLogoutPageGeneratingFilter(); + @Override public void init(H http) throws Exception { - this.loginPageGeneratingFilter.setResolveHiddenInputs( request -> { + Function> hiddenInputs = request -> { CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); - if(token == null) { + if (token == null) { return Collections.emptyMap(); } return Collections.singletonMap(token.getParameterName(), token.getToken()); - }); + }; + this.loginPageGeneratingFilter.setResolveHiddenInputs(hiddenInputs); + this.logoutPageGeneratingFilter.setResolveHiddenInputs(hiddenInputs); http.setSharedObject(DefaultLoginPageGeneratingFilter.class, loginPageGeneratingFilter); } @@ -92,7 +100,8 @@ public void configure(H http) throws Exception { if (loginPageGeneratingFilter.isEnabled() && authenticationEntryPoint == null) { loginPageGeneratingFilter = postProcess(loginPageGeneratingFilter); http.addFilter(loginPageGeneratingFilter); + http.addFilter(this.logoutPageGeneratingFilter); } } -} \ No newline at end of file +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java index 84665715fed..9fa3ad9c470 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 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,6 +23,7 @@ import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.AccessDeniedHandlerImpl; import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.security.web.access.RequestMatcherDelegatingAccessDeniedHandler; import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; @@ -70,6 +71,8 @@ public final class ExceptionHandlingConfigurer> private LinkedHashMap defaultEntryPointMappings = new LinkedHashMap<>(); + private LinkedHashMap defaultDeniedHandlerMappings = new LinkedHashMap<>(); + /** * Creates a new instance * @see HttpSecurity#exceptionHandling() @@ -104,6 +107,26 @@ public ExceptionHandlingConfigurer accessDeniedHandler( return this; } + /** + * Sets a default {@link AccessDeniedHandler} to be used which prefers being + * invoked for the provided {@link RequestMatcher}. If only a single default + * {@link AccessDeniedHandler} is specified, it will be what is used for the + * default {@link AccessDeniedHandler}. If multiple default + * {@link AccessDeniedHandler} instances are configured, then a + * {@link RequestMatcherDelegatingAccessDeniedHandler} will be used. + * + * @param deniedHandler the {@link AccessDeniedHandler} to use + * @param preferredMatcher the {@link RequestMatcher} for this default + * {@link AccessDeniedHandler} + * @return the {@link ExceptionHandlingConfigurer} for further customizations + * @since 5.1 + */ + public ExceptionHandlingConfigurer defaultAccessDeniedHandlerFor( + AccessDeniedHandler deniedHandler, RequestMatcher preferredMatcher) { + this.defaultDeniedHandlerMappings.put(preferredMatcher, deniedHandler); + return this; + } + /** * Sets the {@link AuthenticationEntryPoint} to be used. * @@ -169,13 +192,27 @@ public void configure(H http) throws Exception { AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http); ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter( entryPoint, getRequestCache(http)); - if (accessDeniedHandler != null) { - exceptionTranslationFilter.setAccessDeniedHandler(accessDeniedHandler); - } + AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http); + exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler); exceptionTranslationFilter = postProcess(exceptionTranslationFilter); http.addFilter(exceptionTranslationFilter); } + /** + * Gets the {@link AccessDeniedHandler} according to the rules specified by + * {@link #accessDeniedHandler(AccessDeniedHandler)} + * @param http the {@link HttpSecurity} used to look up shared + * {@link AccessDeniedHandler} + * @return the {@link AccessDeniedHandler} to use + */ + AccessDeniedHandler getAccessDeniedHandler(H http) { + AccessDeniedHandler deniedHandler = this.accessDeniedHandler; + if (deniedHandler == null) { + deniedHandler = createDefaultDeniedHandler(http); + } + return deniedHandler; + } + /** * Gets the {@link AuthenticationEntryPoint} according to the rules specified by * {@link #authenticationEntryPoint(AuthenticationEntryPoint)} @@ -191,16 +228,28 @@ AuthenticationEntryPoint getAuthenticationEntryPoint(H http) { return entryPoint; } + private AccessDeniedHandler createDefaultDeniedHandler(H http) { + if (this.defaultDeniedHandlerMappings.isEmpty()) { + return new AccessDeniedHandlerImpl(); + } + if (this.defaultDeniedHandlerMappings.size() == 1) { + return this.defaultDeniedHandlerMappings.values().iterator().next(); + } + return new RequestMatcherDelegatingAccessDeniedHandler( + this.defaultDeniedHandlerMappings, + new AccessDeniedHandlerImpl()); + } + private AuthenticationEntryPoint createDefaultEntryPoint(H http) { - if (defaultEntryPointMappings.isEmpty()) { + if (this.defaultEntryPointMappings.isEmpty()) { return new Http403ForbiddenEntryPoint(); } - if (defaultEntryPointMappings.size() == 1) { - return defaultEntryPointMappings.values().iterator().next(); + if (this.defaultEntryPointMappings.size() == 1) { + return this.defaultEntryPointMappings.values().iterator().next(); } DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint( - defaultEntryPointMappings); - entryPoint.setDefaultEntryPoint(defaultEntryPointMappings.values().iterator() + this.defaultEntryPointMappings); + entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator() .next()); return entryPoint; } 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 7af9c9c441c..ba8652840cf 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-2016 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.security.config.annotation.web.configurers; import java.net.URI; @@ -58,6 +59,7 @@ * @author Tim Ysewyn * @author Joe Grandja * @author EddĂș MelĂ©ndez + * @author Vedran Pavic * @since 3.2 */ public class HeadersConfigurer> extends @@ -82,6 +84,8 @@ public class HeadersConfigurer> extends private final ReferrerPolicyConfig referrerPolicy = new ReferrerPolicyConfig(); + private final FeaturePolicyConfig featurePolicy = new FeaturePolicyConfig(); + /** * Creates a new instance * @@ -442,7 +446,7 @@ public HeadersConfigurer deny() { * application. *

      * - * @return + * @return the {@link HeadersConfigurer} for additional customization. */ public HeadersConfigurer sameOrigin() { writer = new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN); @@ -775,6 +779,7 @@ private List getHeaderWriters() { addIfNotNull(writers, hpkp.writer); addIfNotNull(writers, contentSecurityPolicy.writer); addIfNotNull(writers, referrerPolicy.writer); + addIfNotNull(writers, featurePolicy.writer); writers.addAll(headerWriters); return writers; } @@ -848,4 +853,44 @@ public HeadersConfigurer and() { } } + + /** + * Allows configuration for Feature + * Policy. + *

      + * Calling this method automatically enables (includes) the {@code Feature-Policy} + * header in the response using the supplied policy directive(s). + *

      + * Configuration is provided to the {@link FeaturePolicyHeaderWriter} which is + * responsible for writing the header. + * + * @see FeaturePolicyHeaderWriter + * @since 5.1 + * @return the {@link FeaturePolicyHeaderWriter} for additional configuration + * @throws IllegalArgumentException if policyDirectives is {@code null} or empty + */ + public FeaturePolicyConfig featurePolicy(String policyDirectives) { + this.featurePolicy.writer = new FeaturePolicyHeaderWriter(policyDirectives); + return featurePolicy; + } + + public final class FeaturePolicyConfig { + + private FeaturePolicyHeaderWriter writer; + + private FeaturePolicyConfig() { + } + + /** + * Allows completing configuration of Feature Policy and continuing configuration + * of headers. + * + * @return the {@link HeadersConfigurer} for additional configuration + */ + public HeadersConfigurer and() { + return HeadersConfigurer.this; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurer.java index 262bb1436c7..0a64f0e5ab8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurer.java @@ -45,7 +45,7 @@ public PortMapperConfigurer() { /** * Allows specifying the {@link PortMapper} instance. * @param portMapper - * @return + * @return the {@link PortMapperConfigurer} for further customizations */ public PortMapperConfigurer portMapper(PortMapper portMapper) { this.portMapper = portMapper; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java index 628b9eca21d..4cc62c887b3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java @@ -19,6 +19,8 @@ import java.util.Collections; import java.util.List; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; import org.springframework.http.MediaType; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -113,11 +115,26 @@ private RequestCache getRequestCache(H http) { if (result != null) { return result; } + result = getBeanOrNull(RequestCache.class); + if (result != null) { + return result; + } HttpSessionRequestCache defaultCache = new HttpSessionRequestCache(); defaultCache.setRequestMatcher(createDefaultSavedRequestMatcher(http)); return defaultCache; } + private T getBeanOrNull(Class type) { + ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); + if (context == null) { + return null; + } + try { + return context.getBean(type); + } catch (NoSuchBeanDefinitionException e) { + return null; + } + } @SuppressWarnings("unchecked") private RequestMatcher createDefaultSavedRequestMatcher(H http) { ContentNegotiationStrategy contentNegotiationStrategy = http 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 fd814edc7d7..43ea86d7f01 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-2016 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. @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; - import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -105,7 +104,7 @@ public final class SessionManagementConfigurer> private Integer maximumSessions; private String expiredUrl; private boolean maxSessionsPreventsLogin; - private SessionCreationPolicy sessionPolicy = SessionCreationPolicy.IF_REQUIRED; + private SessionCreationPolicy sessionPolicy; private boolean enableSessionUrlRewriting; private String invalidSessionUrl; private String sessionAuthenticationErrorUrl; @@ -549,7 +548,14 @@ AuthenticationFailureHandler getSessionAuthenticationFailureHandler() { * @return the {@link SessionCreationPolicy} */ SessionCreationPolicy getSessionCreationPolicy() { - return this.sessionPolicy; + if (this.sessionPolicy != null) { + return this.sessionPolicy; + } + + SessionCreationPolicy sessionPolicy = + getBuilder().getSharedObject(SessionCreationPolicy.class); + return sessionPolicy == null ? + SessionCreationPolicy.IF_REQUIRED : sessionPolicy; } /** @@ -558,8 +564,9 @@ SessionCreationPolicy getSessionCreationPolicy() { * @return true if the {@link SessionCreationPolicy} allows session creation */ private boolean isAllowSessionCreation() { - return SessionCreationPolicy.ALWAYS == this.sessionPolicy - || SessionCreationPolicy.IF_REQUIRED == this.sessionPolicy; + SessionCreationPolicy sessionPolicy = getSessionCreationPolicy(); + return SessionCreationPolicy.ALWAYS == sessionPolicy + || SessionCreationPolicy.IF_REQUIRED == sessionPolicy; } /** @@ -567,7 +574,8 @@ private boolean isAllowSessionCreation() { * @return */ private boolean isStateless() { - return SessionCreationPolicy.STATELESS == this.sessionPolicy; + SessionCreationPolicy sessionPolicy = getSessionCreationPolicy(); + return SessionCreationPolicy.STATELESS == sessionPolicy; } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java index edd17b07119..88ac8eb547d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java @@ -99,7 +99,7 @@ public UrlAuthorizationConfigurer(ApplicationContext context) { * The StandardInterceptUrlRegistry is what users will interact with after applying * the {@link UrlAuthorizationConfigurer}. * - * @return + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customizations */ public StandardInterceptUrlRegistry getRegistry() { return REGISTRY; @@ -198,7 +198,7 @@ FilterInvocationSecurityMetadataSource createMetadataSource(H http) { * provided {@link ConfigAttribute} instances * @param configAttributes the {@link ConfigAttribute} instances that should be mapped * by the {@link RequestMatcher} instances - * @return the {@link UrlAuthorizationConfigurer} for further customizations + * @return the {@link ExpressionUrlAuthorizationConfigurer} for further customizations */ private StandardInterceptUrlRegistry addMapping( Iterable requestMatchers, diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java index 60b5e498558..433119b453c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java @@ -15,9 +15,14 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.security.config.annotation.ObjectPostProcessor; +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.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; /** * An {@link AbstractHttpConfigurer} that provides support for the @@ -29,24 +34,72 @@ * @see OAuth2ClientConfigurer * @see AbstractHttpConfigurer */ -public final class OAuth2Configurer extends AbstractHttpConfigurer { +public final class OAuth2Configurer> + extends AbstractHttpConfigurer, B> { + + @Autowired + private ObjectPostProcessor objectPostProcessor; + + private OAuth2ClientConfigurer clientConfigurer; + + private OAuth2ResourceServerConfigurer resourceServerConfigurer; /** * Returns the {@link OAuth2ClientConfigurer} for configuring OAuth 2.0 Client support. * * @return the {@link OAuth2ClientConfigurer} - * @throws Exception */ - public OAuth2ClientConfigurer client() throws Exception { - return this.getOrApply(new OAuth2ClientConfigurer<>()); + public OAuth2ClientConfigurer client() { + if (this.clientConfigurer == null) { + this.initClientConfigurer(); + } + return this.clientConfigurer; } - @SuppressWarnings("unchecked") - private > C getOrApply(C configurer) throws Exception { - C existingConfigurer = (C) this.getBuilder().getConfigurer(configurer.getClass()); - if (existingConfigurer != null) { - return existingConfigurer; + /** + * Returns the {@link OAuth2ResourceServerConfigurer} for configuring OAuth 2.0 Resource Server support. + * + * @return the {@link OAuth2ResourceServerConfigurer} + */ + public OAuth2ResourceServerConfigurer resourceServer() { + if (this.resourceServerConfigurer == null) { + this.initResourceServerConfigurer(); } - return this.getBuilder().apply(configurer); + return this.resourceServerConfigurer; + } + + @Override + public void init(B builder) throws Exception { + if (this.clientConfigurer != null) { + this.clientConfigurer.init(builder); + } + + if (this.resourceServerConfigurer != null) { + this.resourceServerConfigurer.init(builder); + } + } + + @Override + public void configure(B builder) throws Exception { + if (this.clientConfigurer != null) { + this.clientConfigurer.configure(builder); + } + + if (this.resourceServerConfigurer != null) { + this.resourceServerConfigurer.configure(builder); + } + } + + private void initClientConfigurer() { + this.clientConfigurer = new OAuth2ClientConfigurer<>(); + this.clientConfigurer.setBuilder(this.getBuilder()); + this.clientConfigurer.addObjectPostProcessor(this.objectPostProcessor); + } + + private void initResourceServerConfigurer() { + ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); + this.resourceServerConfigurer = new OAuth2ResourceServerConfigurer<>(context); + this.resourceServerConfigurer.setBuilder(this.getBuilder()); + this.resourceServerConfigurer.addObjectPostProcessor(this.objectPostProcessor); } } 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 e7e6b47a80b..5ff7e2975c6 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 @@ -24,9 +24,13 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.util.Assert; @@ -61,7 +65,7 @@ * *
        *
      • {@link ClientRegistrationRepository} (required)
      • - *
      • {@link OAuth2AuthorizedClientService} (optional)
      • + *
      • {@link OAuth2AuthorizedClientRepository} (optional)
      • *
      * *

      Shared Objects Used

      @@ -70,7 +74,7 @@ * *
        *
      • {@link ClientRegistrationRepository}
      • - *
      • {@link OAuth2AuthorizedClientService}
      • + *
      • {@link OAuth2AuthorizedClientRepository}
      • *
      * * @author Joe Grandja @@ -78,7 +82,7 @@ * @see OAuth2AuthorizationRequestRedirectFilter * @see OAuth2AuthorizationCodeGrantFilter * @see ClientRegistrationRepository - * @see OAuth2AuthorizedClientService + * @see OAuth2AuthorizedClientRepository * @see AbstractHttpConfigurer */ public final class OAuth2ClientConfigurer> extends @@ -98,6 +102,18 @@ public OAuth2ClientConfigurer clientRegistrationRepository(ClientRegistration return this; } + /** + * Sets the repository for authorized client(s). + * + * @param authorizedClientRepository the authorized client repository + * @return the {@link OAuth2ClientConfigurer} for further configuration + */ + public OAuth2ClientConfigurer authorizedClientRepository(OAuth2AuthorizedClientRepository authorizedClientRepository) { + Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); + this.getBuilder().setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository); + return this; + } + /** * Sets the service for authorized client(s). * @@ -106,7 +122,7 @@ public OAuth2ClientConfigurer clientRegistrationRepository(ClientRegistration */ public OAuth2ClientConfigurer authorizedClientService(OAuth2AuthorizedClientService authorizedClientService) { Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); - this.getBuilder().setSharedObject(OAuth2AuthorizedClientService.class, authorizedClientService); + this.authorizedClientRepository(new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService)); return this; } @@ -145,7 +161,8 @@ public AuthorizationEndpointConfig authorizationEndpoint() { * Configuration options for the Authorization Server's Authorization Endpoint. */ public class AuthorizationEndpointConfig { - private String authorizationRequestBaseUri; + private String authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + private OAuth2AuthorizationRequestResolver authorizationRequestResolver; private AuthorizationRequestRepository authorizationRequestRepository; private AuthorizationEndpointConfig() { @@ -163,6 +180,18 @@ public AuthorizationEndpointConfig baseUri(String authorizationRequestBaseUri) { return this; } + /** + * Sets the resolver used for resolving {@link OAuth2AuthorizationRequest}'s. + * + * @param authorizationRequestResolver the resolver used for resolving {@link OAuth2AuthorizationRequest}'s + * @return the {@link AuthorizationEndpointConfig} for further configuration + */ + public AuthorizationEndpointConfig authorizationRequestResolver(OAuth2AuthorizationRequestResolver authorizationRequestResolver) { + Assert.notNull(authorizationRequestResolver, "authorizationRequestResolver cannot be null"); + this.authorizationRequestResolver = authorizationRequestResolver; + return this; + } + /** * Sets the repository used for storing {@link OAuth2AuthorizationRequest}'s. * @@ -185,6 +214,52 @@ public AuthorizationEndpointConfig authorizationRequestRepository( public AuthorizationCodeGrantConfigurer and() { return AuthorizationCodeGrantConfigurer.this; } + + private OAuth2AuthorizationRequestResolver getAuthorizationRequestResolver() { + if (this.authorizationRequestResolver != null) { + return this.authorizationRequestResolver; + } + ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils + .getClientRegistrationRepository(getBuilder()); + return new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, + this.authorizationRequestBaseUri); + } + + private OAuth2AuthorizationRequestRedirectFilter createAuthorizationRequestRedirectFilter(B builder) { + OAuth2AuthorizationRequestResolver resolver = getAuthorizationRequestResolver(); + OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(resolver); + + if (this.authorizationRequestRepository != null) { + authorizationRequestFilter.setAuthorizationRequestRepository( + this.authorizationRequestRepository); + } + RequestCache requestCache = builder.getSharedObject(RequestCache.class); + if (requestCache != null) { + authorizationRequestFilter.setRequestCache(requestCache); + } + return authorizationRequestFilter; + } + + private OAuth2AuthorizationCodeGrantFilter createAuthorizationCodeGrantFilter(B builder) { + AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); + OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter( + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), + OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(builder), + authenticationManager); + + if (this.authorizationRequestRepository != null) { + authorizationCodeGrantFilter.setAuthorizationRequestRepository( + this.authorizationRequestRepository); + } + return authorizationCodeGrantFilter; + } + + private void configure(B builder) { + OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = createAuthorizationRequestRedirectFilter(builder); + builder.addFilter(postProcess(authorizationRequestFilter)); + OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = createAuthorizationCodeGrantFilter(builder); + builder.addFilter(postProcess(authorizationCodeGrantFilter)); + } } /** @@ -237,6 +312,10 @@ public AuthorizationCodeGrantConfigurer and() { public OAuth2ClientConfigurer and() { return OAuth2ClientConfigurer.this; } + + private void configure(B builder) { + this.authorizationEndpointConfig.configure(builder); + } } @Override @@ -249,7 +328,7 @@ public void init(B builder) throws Exception { @Override public void configure(B builder) throws Exception { if (this.authorizationCodeGrantConfigurer != null) { - this.configure(builder, this.authorizationCodeGrantConfigurer); + this.authorizationCodeGrantConfigurer.configure(builder); } } @@ -264,37 +343,4 @@ private void init(B builder, AuthorizationCodeGrantConfigurer authorizationCodeG new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient); builder.authenticationProvider(this.postProcess(authorizationCodeAuthenticationProvider)); } - - private void configure(B builder, AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer) throws Exception { - String authorizationRequestBaseUri = authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestBaseUri; - if (authorizationRequestBaseUri == null) { - authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; - } - - OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( - OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), authorizationRequestBaseUri); - - if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository != null) { - authorizationRequestFilter.setAuthorizationRequestRepository( - authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository); - } - RequestCache requestCache = builder.getSharedObject(RequestCache.class); - if (requestCache != null) { - authorizationRequestFilter.setRequestCache(requestCache); - } - builder.addFilter(this.postProcess(authorizationRequestFilter)); - - AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); - - OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter( - OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), - OAuth2ClientConfigurerUtils.getAuthorizedClientService(builder), - authenticationManager); - - if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository != null) { - authorizationCodeGrantFilter.setAuthorizationRequestRepository( - authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository); - } - builder.addFilter(this.postProcess(authorizationCodeGrantFilter)); - } } 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 646c32accfd..140ad70c321 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 @@ -23,6 +23,9 @@ import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.util.StringUtils; import java.util.Map; @@ -50,14 +53,35 @@ private static > ClientRegistrationRepository g return builder.getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class); } - static > OAuth2AuthorizedClientService getAuthorizedClientService(B builder) { - OAuth2AuthorizedClientService authorizedClientService = builder.getSharedObject(OAuth2AuthorizedClientService.class); - if (authorizedClientService == null) { - authorizedClientService = getAuthorizedClientServiceBean(builder); - if (authorizedClientService == null) { - authorizedClientService = new InMemoryOAuth2AuthorizedClientService(getClientRegistrationRepository(builder)); + static > OAuth2AuthorizedClientRepository getAuthorizedClientRepository(B builder) { + OAuth2AuthorizedClientRepository authorizedClientRepository = builder.getSharedObject(OAuth2AuthorizedClientRepository.class); + if (authorizedClientRepository == null) { + authorizedClientRepository = getAuthorizedClientRepositoryBean(builder); + if (authorizedClientRepository == null) { + authorizedClientRepository = new AuthenticatedPrincipalOAuth2AuthorizedClientRepository( + getAuthorizedClientService((builder))); } - builder.setSharedObject(OAuth2AuthorizedClientService.class, authorizedClientService); + builder.setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository); + } + return authorizedClientRepository; + } + + private static > OAuth2AuthorizedClientRepository getAuthorizedClientRepositoryBean(B builder) { + Map authorizedClientRepositoryMap = BeanFactoryUtils.beansOfTypeIncludingAncestors( + builder.getSharedObject(ApplicationContext.class), OAuth2AuthorizedClientRepository.class); + if (authorizedClientRepositoryMap.size() > 1) { + throw new NoUniqueBeanDefinitionException(OAuth2AuthorizedClientRepository.class, authorizedClientRepositoryMap.size(), + "Expected single matching bean of type '" + OAuth2AuthorizedClientRepository.class.getName() + "' but found " + + authorizedClientRepositoryMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(authorizedClientRepositoryMap.keySet())); + } + return (!authorizedClientRepositoryMap.isEmpty() ? authorizedClientRepositoryMap.values().iterator().next() : null); + } + + + private static > OAuth2AuthorizedClientService getAuthorizedClientService(B builder) { + OAuth2AuthorizedClientService authorizedClientService = getAuthorizedClientServiceBean(builder); + if (authorizedClientService == null) { + authorizedClientService = new InMemoryOAuth2AuthorizedClientService(getClientRegistrationRepository(builder)); } return authorizedClientService; } @@ -67,7 +91,8 @@ private static > OAuth2AuthorizedClientService builder.getSharedObject(ApplicationContext.class), OAuth2AuthorizedClientService.class); if (authorizedClientServiceMap.size() > 1) { throw new NoUniqueBeanDefinitionException(OAuth2AuthorizedClientService.class, authorizedClientServiceMap.size(), - "Only one matching @Bean of type " + OAuth2AuthorizedClientService.class.getName() + " should be registered."); + "Expected single matching bean of type '" + OAuth2AuthorizedClientService.class.getName() + "' but found " + + authorizedClientServiceMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(authorizedClientServiceMap.keySet())); } return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null); } 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 972afc1443a..b84f4bb390b 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-2017 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. @@ -42,8 +42,11 @@ import org.springframework.security.oauth2.client.userinfo.DelegatingOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -51,15 +54,23 @@ import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.NegatedRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -91,7 +102,7 @@ * *
        *
      • {@link ClientRegistrationRepository} (required)
      • - *
      • {@link OAuth2AuthorizedClientService} (optional)
      • + *
      • {@link OAuth2AuthorizedClientRepository} (optional)
      • *
      • {@link GrantedAuthoritiesMapper} (optional)
      • *
      * @@ -101,7 +112,7 @@ * *
        *
      • {@link ClientRegistrationRepository}
      • - *
      • {@link OAuth2AuthorizedClientService}
      • + *
      • {@link OAuth2AuthorizedClientRepository}
      • *
      • {@link GrantedAuthoritiesMapper}
      • *
      • {@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not configured * and {@code DefaultLoginPageGeneratingFilter} is available, than a default login page will be made available
      • @@ -114,6 +125,7 @@ * @see OAuth2AuthorizationRequestRedirectFilter * @see OAuth2LoginAuthenticationFilter * @see ClientRegistrationRepository + * @see OAuth2AuthorizedClientRepository * @see AbstractAuthenticationFilterConfigurer */ public final class OAuth2LoginConfigurer> extends @@ -124,6 +136,7 @@ public final class OAuth2LoginConfigurer> exten private final RedirectionEndpointConfig redirectionEndpointConfig = new RedirectionEndpointConfig(); private final UserInfoEndpointConfig userInfoEndpointConfig = new UserInfoEndpointConfig(); private String loginPage; + private String loginProcessingUrl = OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; /** * Sets the repository of client registrations. @@ -137,6 +150,19 @@ public OAuth2LoginConfigurer clientRegistrationRepository(ClientRegistrationR return this; } + /** + * Sets the repository for authorized client(s). + * + * @since 5.1 + * @param authorizedClientRepository the authorized client repository + * @return the {@link OAuth2LoginConfigurer} for further configuration + */ + public OAuth2LoginConfigurer authorizedClientRepository(OAuth2AuthorizedClientRepository authorizedClientRepository) { + Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); + this.getBuilder().setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository); + return this; + } + /** * Sets the service for authorized client(s). * @@ -145,7 +171,7 @@ public OAuth2LoginConfigurer clientRegistrationRepository(ClientRegistrationR */ public OAuth2LoginConfigurer authorizedClientService(OAuth2AuthorizedClientService authorizedClientService) { Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); - this.getBuilder().setSharedObject(OAuth2AuthorizedClientService.class, authorizedClientService); + this.authorizedClientRepository(new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService)); return this; } @@ -156,6 +182,13 @@ public OAuth2LoginConfigurer loginPage(String loginPage) { return this; } + @Override + public OAuth2LoginConfigurer loginProcessingUrl(String loginProcessingUrl) { + Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty"); + this.loginProcessingUrl = loginProcessingUrl; + return this; + } + /** * Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization Server's Authorization Endpoint. * @@ -170,6 +203,7 @@ public AuthorizationEndpointConfig authorizationEndpoint() { */ public class AuthorizationEndpointConfig { private String authorizationRequestBaseUri; + private OAuth2AuthorizationRequestResolver authorizationRequestResolver; private AuthorizationRequestRepository authorizationRequestRepository; private AuthorizationEndpointConfig() { @@ -187,6 +221,19 @@ public AuthorizationEndpointConfig baseUri(String authorizationRequestBaseUri) { return this; } + /** + * Sets the resolver used for resolving {@link OAuth2AuthorizationRequest}'s. + * + * @since 5.1 + * @param authorizationRequestResolver the resolver used for resolving {@link OAuth2AuthorizationRequest}'s + * @return the {@link AuthorizationEndpointConfig} for further configuration + */ + public AuthorizationEndpointConfig authorizationRequestResolver(OAuth2AuthorizationRequestResolver authorizationRequestResolver) { + Assert.notNull(authorizationRequestResolver, "authorizationRequestResolver cannot be null"); + this.authorizationRequestResolver = authorizationRequestResolver; + return this; + } + /** * Sets the repository used for storing {@link OAuth2AuthorizationRequest}'s. * @@ -377,14 +424,28 @@ public void init(B http) throws Exception { OAuth2LoginAuthenticationFilter authenticationFilter = new OAuth2LoginAuthenticationFilter( OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), - OAuth2ClientConfigurerUtils.getAuthorizedClientService(this.getBuilder()), - OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI); + OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(this.getBuilder()), + this.loginProcessingUrl); this.setAuthenticationFilter(authenticationFilter); - this.loginProcessingUrl(OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI); + super.loginProcessingUrl(this.loginProcessingUrl); + if (this.loginPage != null) { + // Set custom login page super.loginPage(this.loginPage); + super.init(http); + } else { + Map loginUrlToClientName = this.getLoginLinks(); + if (loginUrlToClientName.size() == 1) { + // Setup auto-redirect to provider login page + // when only 1 client is configured + this.updateAuthenticationDefaults(); + this.updateAccessDefaults(http); + String providerLoginPage = loginUrlToClientName.keySet().iterator().next(); + this.registerAuthenticationEntryPoint(http, this.getLoginEntryPoint(http, providerLoginPage)); + } else { + super.init(http); + } } - super.init(http); OAuth2AccessTokenResponseClient accessTokenResponseClient = this.tokenEndpointConfig.accessTokenResponseClient; @@ -436,13 +497,19 @@ public void init(B http) throws Exception { @Override public void configure(B http) throws Exception { - String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri; - if (authorizationRequestBaseUri == null) { - authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; - } + OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter; - OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( - OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), authorizationRequestBaseUri); + if (this.authorizationEndpointConfig.authorizationRequestResolver != null) { + authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( + this.authorizationEndpointConfig.authorizationRequestResolver); + } else { + String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri; + if (authorizationRequestBaseUri == null) { + authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + } + authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), authorizationRequestBaseUri); + } if (this.authorizationEndpointConfig.authorizationRequestRepository != null) { authorizationRequestFilter.setAuthorizationRequestRepository( @@ -484,9 +551,9 @@ private GrantedAuthoritiesMapper getGrantedAuthoritiesMapper() { private GrantedAuthoritiesMapper getGrantedAuthoritiesMapperBean() { Map grantedAuthoritiesMapperMap = - BeanFactoryUtils.beansOfTypeIncludingAncestors( - this.getBuilder().getSharedObject(ApplicationContext.class), - GrantedAuthoritiesMapper.class); + BeanFactoryUtils.beansOfTypeIncludingAncestors( + this.getBuilder().getSharedObject(ApplicationContext.class), + GrantedAuthoritiesMapper.class); return (!grantedAuthoritiesMapperMap.isEmpty() ? grantedAuthoritiesMapperMap.values().iterator().next() : null); } @@ -496,29 +563,51 @@ private void initDefaultLoginFilter(B http) { return; } + loginPageGeneratingFilter.setOauth2LoginEnabled(true); + loginPageGeneratingFilter.setOauth2AuthenticationUrlToClientName(this.getLoginLinks()); + loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage()); + loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl()); + } + + @SuppressWarnings("unchecked") + private Map getLoginLinks() { Iterable clientRegistrations = null; ClientRegistrationRepository clientRegistrationRepository = - OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()); + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()); ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class); if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) { clientRegistrations = (Iterable) clientRegistrationRepository; } if (clientRegistrations == null) { - return; + return Collections.emptyMap(); } String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri != null ? - this.authorizationEndpointConfig.authorizationRequestBaseUri : - OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; - Map authenticationUrlToClientName = new HashMap<>(); + this.authorizationEndpointConfig.authorizationRequestBaseUri : + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + Map loginUrlToClientName = new HashMap<>(); + clientRegistrations.forEach(registration -> loginUrlToClientName.put( + authorizationRequestBaseUri + "/" + registration.getRegistrationId(), + registration.getClientName())); + + return loginUrlToClientName; + } - clientRegistrations.forEach(registration -> authenticationUrlToClientName.put( - authorizationRequestBaseUri + "/" + registration.getRegistrationId(), - registration.getClientName())); - loginPageGeneratingFilter.setOauth2LoginEnabled(true); - loginPageGeneratingFilter.setOauth2AuthenticationUrlToClientName(authenticationUrlToClientName); - loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage()); - loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl()); + private AuthenticationEntryPoint getLoginEntryPoint(B http, String providerLoginPage) { + RequestMatcher loginPageMatcher = new AntPathRequestMatcher(this.getLoginPage()); + RequestMatcher faviconMatcher = new AntPathRequestMatcher("/favicon.ico"); + RequestMatcher defaultEntryPointMatcher = this.getAuthenticationEntryPointMatcher(http); + RequestMatcher defaultLoginPageMatcher = new AndRequestMatcher( + new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher); + + LinkedHashMap entryPoints = new LinkedHashMap<>(); + entryPoints.put(new NegatedRequestMatcher(defaultLoginPageMatcher), + new LoginUrlAuthenticationEntryPoint(providerLoginPage)); + + DelegatingAuthenticationEntryPoint loginEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints); + loginEntryPoint.setDefaultEntryPoint(this.getAuthenticationEntryPoint()); + + return loginEntryPoint; } private static class OidcAuthenticationRequestChecker implements AuthenticationProvider { 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 new file mode 100644 index 00000000000..d03662615e2 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -0,0 +1,294 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers.oauth2.server.resource; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.context.ApplicationContext; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +/** + * + * An {@link AbstractHttpConfigurer} for OAuth 2.0 Resource Server Support. + * + * By default, this wires a {@link BearerTokenAuthenticationFilter}, which can be used to parse the request + * for bearer tokens and make an authentication attempt. + * + *

        + * The following configuration options are available: + * + *

          + *
        • {@link #accessDeniedHandler(AccessDeniedHandler)}
        • - customizes how access denied errors are handled + *
        • {@link #authenticationEntryPoint(AuthenticationEntryPoint)}
        • - customizes how authentication failures are handled + *
        • {@link #bearerTokenResolver(BearerTokenResolver)} - customizes how to resolve a bearer token from the request
        • + *
        • {@link #jwt()} - enables Jwt-encoded bearer token support
        • + *
        + * + *

        + * When using {@link #jwt()}, either + * + *

          + *
        • + * supply a Jwk Set Uri via {@link JwtConfigurer#jwkSetUri}, or + *
        • + *
        • + * supply a {@link JwtDecoder} instance via {@link JwtConfigurer#decoder}, or + *
        • + *
        • + * expose a {@link JwtDecoder} bean + *
        • + *
        + * + *

        Security Filters

        + * + * The following {@code Filter}s are populated when {@link #jwt()} is configured: + * + *
          + *
        • {@link BearerTokenAuthenticationFilter}
        • + *
        + * + *

        Shared Objects Created

        + * + * The following shared objects are populated: + * + *
          + *
        • {@link SessionCreationPolicy} (optional)
        • + *
        + * + *

        Shared Objects Used

        + * + * The following shared objects are used: + * + *
          + *
        • {@link AuthenticationManager}
        • + *
        + * + * @author Josh Cummings + * @since 5.1 + * @see BearerTokenAuthenticationFilter + * @see JwtAuthenticationProvider + * @see NimbusJwtDecoderJwkSupport + * @see AbstractHttpConfigurer + */ +public final class OAuth2ResourceServerConfigurer> extends + AbstractHttpConfigurer, H> { + + private final ApplicationContext context; + + private BearerTokenResolver bearerTokenResolver; + private JwtConfigurer jwtConfigurer; + + private AccessDeniedHandler accessDeniedHandler = new BearerTokenAccessDeniedHandler(); + private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint(); + private BearerTokenRequestMatcher requestMatcher = new BearerTokenRequestMatcher(); + + public OAuth2ResourceServerConfigurer(ApplicationContext context) { + Assert.notNull(context, "context cannot be null"); + this.context = context; + } + + public OAuth2ResourceServerConfigurer accessDeniedHandler(AccessDeniedHandler accessDeniedHandler) { + Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null"); + this.accessDeniedHandler = accessDeniedHandler; + return this; + } + + public OAuth2ResourceServerConfigurer authenticationEntryPoint(AuthenticationEntryPoint entryPoint) { + Assert.notNull(entryPoint, "entryPoint cannot be null"); + this.authenticationEntryPoint = entryPoint; + return this; + } + + public OAuth2ResourceServerConfigurer bearerTokenResolver(BearerTokenResolver bearerTokenResolver) { + Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); + this.bearerTokenResolver = bearerTokenResolver; + return this; + } + + public JwtConfigurer jwt() { + if ( this.jwtConfigurer == null ) { + this.jwtConfigurer = new JwtConfigurer(this.context); + } + + return this.jwtConfigurer; + } + + @Override + public void setBuilder(H http) { + super.setBuilder(http); + initSessionCreationPolicy(http); + } + + @Override + public void init(H http) throws Exception { + registerDefaultAccessDeniedHandler(http); + registerDefaultEntryPoint(http); + registerDefaultCsrfOverride(http); + } + + @Override + public void configure(H http) throws Exception { + BearerTokenResolver bearerTokenResolver = getBearerTokenResolver(); + this.requestMatcher.setBearerTokenResolver(bearerTokenResolver); + + AuthenticationManager manager = http.getSharedObject(AuthenticationManager.class); + + BearerTokenAuthenticationFilter filter = + new BearerTokenAuthenticationFilter(manager); + filter.setBearerTokenResolver(bearerTokenResolver); + filter.setAuthenticationEntryPoint(this.authenticationEntryPoint); + filter = postProcess(filter); + + http.addFilter(filter); + + if ( this.jwtConfigurer == null ) { + throw new IllegalStateException("Jwt is the only supported format for bearer tokens " + + "in Spring Security and no Jwt configuration was found. Make sure to specify " + + "a jwk set uri by doing http.oauth2().resourceServer().jwt().jwkSetUri(uri), or wire a " + + "JwtDecoder instance by doing http.oauth2().resourceServer().jwt().decoder(decoder), or " + + "expose a JwtDecoder instance as a bean and do http.oauth2().resourceServer().jwt()."); + } + + JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder(); + + JwtAuthenticationProvider provider = + new JwtAuthenticationProvider(decoder); + provider = postProcess(provider); + + http.authenticationProvider(provider); + } + + public class JwtConfigurer { + private final ApplicationContext context; + + private JwtDecoder decoder; + + JwtConfigurer(ApplicationContext context) { + this.context = context; + } + + public JwtConfigurer decoder(JwtDecoder decoder) { + this.decoder = decoder; + return this; + } + + public JwtConfigurer jwkSetUri(String uri) { + this.decoder = new NimbusJwtDecoderJwkSupport(uri); + return this; + } + + public OAuth2ResourceServerConfigurer and() { + return OAuth2ResourceServerConfigurer.this; + } + + JwtDecoder getJwtDecoder() { + if ( this.decoder == null ) { + return this.context.getBean(JwtDecoder.class); + } + + return this.decoder; + } + } + + private void initSessionCreationPolicy(H http) { + if (http.getSharedObject(SessionCreationPolicy.class) == null) { + http.setSharedObject(SessionCreationPolicy.class, SessionCreationPolicy.STATELESS); + } + } + + private void registerDefaultAccessDeniedHandler(H http) { + ExceptionHandlingConfigurer exceptionHandling = http + .getConfigurer(ExceptionHandlingConfigurer.class); + if (exceptionHandling == null) { + return; + } + + exceptionHandling.defaultAccessDeniedHandlerFor( + this.accessDeniedHandler, + this.requestMatcher); + } + + private void registerDefaultEntryPoint(H http) { + ExceptionHandlingConfigurer exceptionHandling = http + .getConfigurer(ExceptionHandlingConfigurer.class); + if (exceptionHandling == null) { + return; + } + + exceptionHandling.defaultAuthenticationEntryPointFor( + this.authenticationEntryPoint, + this.requestMatcher); + } + + private void registerDefaultCsrfOverride(H http) { + CsrfConfigurer csrf = http + .getConfigurer(CsrfConfigurer.class); + if (csrf == null) { + return; + } + + csrf.ignoringRequestMatchers(this.requestMatcher); + } + + BearerTokenResolver getBearerTokenResolver() { + if ( this.bearerTokenResolver == null ) { + if ( this.context.getBeanNamesForType(BearerTokenResolver.class).length > 0 ) { + this.bearerTokenResolver = this.context.getBean(BearerTokenResolver.class); + } else { + this.bearerTokenResolver = new DefaultBearerTokenResolver(); + } + } + + return this.bearerTokenResolver; + } + + private static final class BearerTokenRequestMatcher implements RequestMatcher { + private BearerTokenResolver bearerTokenResolver; + + @Override + public boolean matches(HttpServletRequest request) { + try { + return this.bearerTokenResolver.resolve(request) != null; + } catch ( OAuth2AuthenticationException e ) { + return false; + } + } + + public void setBearerTokenResolver(BearerTokenResolver tokenResolver) { + Assert.notNull(tokenResolver, "resolver cannot be null"); + this.bearerTokenResolver = tokenResolver; + } + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java index a1c8ae02253..e29ccdee920 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java @@ -479,7 +479,7 @@ public AttributeConfigurer required(boolean required) { /** * The OpenID attribute type. * @param type - * @return + * @return the {@link AttributeConfigurer} for further customizations */ public AttributeConfigurer type(String type) { this.type = type; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java index c5d2ddb6dac..6c0603049dc 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ReactiveOAuth2ClientImportSelector.java @@ -16,19 +16,18 @@ package org.springframework.security.config.annotation.web.reactive; -import java.util.List; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2ClientArgumentResolver; +import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver; import org.springframework.util.ClassUtils; import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; +import java.util.List; + /** * {@link Configuration} for OAuth 2.0 Client support. * @@ -52,21 +51,12 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { @Configuration static class OAuth2ClientWebFluxSecurityConfiguration implements WebFluxConfigurer { - private ReactiveClientRegistrationRepository clientRegistrationRepository; - private ReactiveOAuth2AuthorizedClientService authorizedClientService; @Override public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { - if (this.clientRegistrationRepository != null && this.authorizedClientService != null) { - configurer.addCustomResolver(new OAuth2ClientArgumentResolver(this.clientRegistrationRepository, this.authorizedClientService)); - } - } - - @Autowired(required = false) - public void setClientRegistrationRepository(List clientRegistrationRepository) { - if (clientRegistrationRepository.size() == 1) { - this.clientRegistrationRepository = clientRegistrationRepository.get(0); + if (this.authorizedClientService != null) { + configurer.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver(this.authorizedClientService)); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java index 8e069900924..d5c63002a88 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java @@ -22,12 +22,14 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver; @@ -38,6 +40,7 @@ * @author Rob Winch * @since 5.0 */ +@Configuration class ServerHttpSecurityConfiguration implements WebFluxConfigurer { private static final String BEAN_NAME_PREFIX = "org.springframework.security.config.annotation.web.reactive.HttpSecurityConfiguration."; private static final String HTTPSECURITY_BEAN_NAME = BEAN_NAME_PREFIX + "httpSecurity"; @@ -54,6 +57,9 @@ class ServerHttpSecurityConfiguration implements WebFluxConfigurer { @Autowired(required = false) private PasswordEncoder passwordEncoder; + @Autowired(required = false) + private ReactiveUserDetailsPasswordService userDetailsPasswordService; + @Autowired(required = false) private BeanFactory beanFactory; @@ -92,6 +98,7 @@ private ReactiveAuthenticationManager authenticationManager() { if(this.passwordEncoder != null) { manager.setPasswordEncoder(this.passwordEncoder); } + manager.setUserDetailsPasswordService(this.userDetailsPasswordService); return manager; } return null; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java index bed91d4f973..30b2b44b7f4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/WebFluxSecurityConfiguration.java @@ -87,13 +87,14 @@ private SecurityWebFilterChain springSecurityFilterChain() { private SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http .authorizeExchange() - .anyExchange().authenticated() - .and() - .httpBasic().and() - .formLogin(); + .anyExchange().authenticated(); - if (isOAuth2Present) { + if (isOAuth2Present && OAuth2ClasspathGuard.shouldConfigure(this.context)) { OAuth2ClasspathGuard.configure(this.context, http); + } else { + http + .httpBasic().and() + .formLogin(); } SecurityWebFilterChain result = http.build(); @@ -102,11 +103,13 @@ private SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http private static class OAuth2ClasspathGuard { static void configure(ApplicationContext context, ServerHttpSecurity http) { + http.oauth2Login(); + } + + static boolean shouldConfigure(ApplicationContext context) { ClassLoader loader = context.getClassLoader(); Class reactiveClientRegistrationRepositoryClass = ClassUtils.resolveClassName(REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME, loader); - if (context.getBeanNamesForType(reactiveClientRegistrationRepositoryClass).length == 1) { - http.oauth2Login(); - } + return context.getBeanNamesForType(reactiveClientRegistrationRepositoryClass).length == 1; } } } diff --git a/config/src/main/java/org/springframework/security/config/core/userdetails/ReactiveUserDetailsServiceResourceFactoryBean.java b/config/src/main/java/org/springframework/security/config/core/userdetails/ReactiveUserDetailsServiceResourceFactoryBean.java index e932cc17c15..2f74d105a3d 100644 --- a/config/src/main/java/org/springframework/security/config/core/userdetails/ReactiveUserDetailsServiceResourceFactoryBean.java +++ b/config/src/main/java/org/springframework/security/config/core/userdetails/ReactiveUserDetailsServiceResourceFactoryBean.java @@ -57,7 +57,6 @@ public void setResourceLoader(ResourceLoader resourceLoader) { * Sets the location of a Resource that is a Properties file in the format defined in {@link UserDetailsResourceFactoryBean}. * * @param resourceLocation the location of the properties file that contains the users (i.e. "classpath:users.properties") - * @return the UserDetailsResourceFactoryBean */ public void setResourceLocation(String resourceLocation) { this.userDetails.setResourceLocation(resourceLocation); 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 39bcdbf434c..3ad241ccb31 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 @@ -48,6 +48,7 @@ import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor; import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.CsrfToken; @@ -123,6 +124,7 @@ final class AuthenticationConfigBuilder { @SuppressWarnings("rawtypes") private ManagedList logoutHandlers; private BeanDefinition loginPageGenerationFilter; + private BeanDefinition logoutPageGenerationFilter; private BeanDefinition etf; private final BeanReference requestCache; private final BeanReference portMapper; @@ -544,6 +546,10 @@ void createLoginPageFilterIfNeeded() { .rootBeanDefinition(DefaultLoginPageGeneratingFilter.class); loginPageFilter.addPropertyValue("resolveHiddenInputs", new CsrfTokenHiddenInputFunction()); + BeanDefinitionBuilder logoutPageFilter = BeanDefinitionBuilder + .rootBeanDefinition(DefaultLogoutPageGeneratingFilter.class); + logoutPageFilter.addPropertyValue("resolveHiddenInputs", new CsrfTokenHiddenInputFunction()); + if (formFilterId != null) { loginPageFilter.addConstructorArgReference(formFilterId); loginPageFilter.addPropertyValue("authenticationUrl", loginProcessingUrl); @@ -556,6 +562,7 @@ void createLoginPageFilterIfNeeded() { } loginPageGenerationFilter = loginPageFilter.getBeanDefinition(); + this.logoutPageGenerationFilter = logoutPageFilter.getBeanDefinition(); } } @@ -798,6 +805,7 @@ List getFilters() { if (loginPageGenerationFilter != null) { filters.add(new OrderDecorator(loginPageGenerationFilter, LOGIN_PAGE_FILTER)); + filters.add(new OrderDecorator(this.logoutPageGenerationFilter, LOGOUT_PAGE_FILTER)); } if (basicFilter != null) { diff --git a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java index 20ac10dab7e..dffeab94a09 100644 --- a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.security.config.http; import java.net.URI; @@ -47,6 +48,7 @@ * @author Marten Deinum * @author Tim Ysewyn * @author EddĂș MelĂ©ndez + * @author Vedran Pavic * @since 3.2 */ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { @@ -85,6 +87,7 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { private static final String CONTENT_SECURITY_POLICY_ELEMENT = "content-security-policy"; private static final String REFERRER_POLICY_ELEMENT = "referrer-policy"; + private static final String FEATURE_POLICY_ELEMENT = "feature-policy"; private static final String ALLOW_FROM = "ALLOW-FROM"; @@ -114,6 +117,8 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { parseReferrerPolicyElement(element, parserContext); + parseFeaturePolicyElement(element, parserContext); + parseHeaderElements(element); boolean noWriters = headerWriters.isEmpty(); @@ -313,6 +318,32 @@ private void addReferrerPolicy(Element referrerPolicyElement, ParserContext cont headerWriters.add(headersWriter.getBeanDefinition()); } + private void parseFeaturePolicyElement(Element element, ParserContext context) { + Element featurePolicyElement = (element == null) ? null + : DomUtils.getChildElementByTagName(element, FEATURE_POLICY_ELEMENT); + if (featurePolicyElement != null) { + addFeaturePolicy(featurePolicyElement, context); + } + } + + private void addFeaturePolicy(Element featurePolicyElement, ParserContext context) { + BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder + .genericBeanDefinition(FeaturePolicyHeaderWriter.class); + + String policyDirectives = featurePolicyElement + .getAttribute(ATT_POLICY_DIRECTIVES); + if (!StringUtils.hasText(policyDirectives)) { + context.getReaderContext().error( + ATT_POLICY_DIRECTIVES + " requires a 'value' to be set.", + featurePolicyElement); + } + else { + headersWriter.addConstructorArgValue(policyDirectives); + } + + headerWriters.add(headersWriter.getBeanDefinition()); + } + private void attrNotAllowed(ParserContext context, String attrName, String otherAttrName, Element element) { context.getReaderContext().error( diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java index 3c01a48f94e..516cf31cfc8 100644 --- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java +++ b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java @@ -40,7 +40,9 @@ enum SecurityFilters { FORM_LOGIN_FILTER, OPENID_FILTER, LOGIN_PAGE_FILTER, + LOGOUT_PAGE_FILTER, DIGEST_AUTH_FILTER, + BEARER_TOKEN_AUTH_FILTER, BASIC_AUTH_FILTER, REQUEST_CACHE_FILTER, SERVLET_API_SUPPORT_FILTER, diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java index 526a05cc5fb..a98aff17261 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java @@ -73,7 +73,7 @@ public Builder getBuilder(String registrationId) { builder.scope("public_profile", "email"); builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth"); builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token"); - builder.userInfoUri("https://graph.facebook.com/me"); + builder.userInfoUri("https://graph.facebook.com/me?fields=id,name,email"); builder.userNameAttributeName("id"); builder.clientName("Facebook"); return builder; diff --git a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java index fc68e28f7d0..3c3d8acf9c4 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java +++ b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java @@ -23,6 +23,10 @@ public enum SecurityWebFiltersOrder { FIRST(Integer.MIN_VALUE), HTTP_HEADERS_WRITER, + /** + * {@link org.springframework.web.cors.reactive.CorsWebFilter} + */ + CORS, /** * {@link org.springframework.security.web.server.csrf.CsrfWebFilter} */ 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 6c8c40622cf..0d2e848269e 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 @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; +import java.security.interfaces.RSAPublicKey; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -35,21 +36,30 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager; import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2LoginReactiveAuthenticationManager; -import org.springframework.security.oauth2.client.endpoint.NimbusReactiveAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.WebClientReactiveAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectWebFilter; import org.springframework.security.oauth2.client.web.ServerOAuth2LoginAuthenticationTokenConverter; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager; +import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint; +import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter; import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint; import org.springframework.security.web.server.MatcherSecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain; @@ -100,6 +110,11 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.web.cors.reactive.CorsConfigurationSource; +import org.springframework.web.cors.reactive.CorsProcessor; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.DefaultCorsProcessor; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; @@ -170,6 +185,8 @@ public class ServerHttpSecurity { private CsrfSpec csrf = new CsrfSpec(); + private CorsSpec cors = new CorsSpec(); + private ExceptionHandlingSpec exceptionHandling = new ExceptionHandlingSpec(); private HttpBasicSpec httpBasic; @@ -180,8 +197,12 @@ public class ServerHttpSecurity { private OAuth2LoginSpec oauth2Login; + private OAuth2Spec oauth2; + private LogoutSpec logout = new LogoutSpec(); + private LoginPageSpec loginPage = new LoginPageSpec(); + private ReactiveAuthenticationManager authenticationManager; private ServerSecurityContextRepository securityContextRepository = new WebSessionServerSecurityContextRepository(); @@ -203,6 +224,7 @@ public class ServerHttpSecurity { * * @param matcher the ServerExchangeMatcher that determines which requests apply to this HttpSecurity instance. * Default is all requests. + * @return the {@link ServerHttpSecurity} to continue configuring */ public ServerHttpSecurity securityMatcher(ServerWebExchangeMatcher matcher) { Assert.notNull(matcher, "matcher cannot be null"); @@ -283,6 +305,80 @@ public CsrfSpec csrf() { return this.csrf; } + /** + * Configures CORS headers. By default if a {@link CorsConfigurationSource} Bean is found, it will be used + * to create a {@link CorsWebFilter}. If {@link CorsSpec#configurationSource(CorsConfigurationSource)} is invoked + * it will be used instead. If neither has been configured, the Cors configuration will do nothing. + * @return the {@link CorsSpec} to customize + */ + public CorsSpec cors() { + if (this.cors == null) { + this.cors = new CorsSpec(); + } + return this.cors; + } + + /** + * Configures CORS support within Spring Security. This ensures that the {@link CorsWebFilter} is place in the + * correct order. + */ + public class CorsSpec { + private CorsWebFilter corsFilter; + + /** + * Configures the {@link CorsConfigurationSource} to be used + * @param source the source to use + * @return the {@link CorsSpec} for additional configuration + */ + public CorsSpec configurationSource(CorsConfigurationSource source) { + this.corsFilter = new CorsWebFilter(source); + return this; + } + + /** + * Disables CORS support within Spring Security. + * @return the {@link ServerHttpSecurity} to continue configuring + */ + public ServerHttpSecurity disable() { + ServerHttpSecurity.this.cors = null; + return ServerHttpSecurity.this; + } + + /** + * Allows method chaining to continue configuring the {@link ServerHttpSecurity} + * @return the {@link ServerHttpSecurity} to continue configuring + */ + public ServerHttpSecurity and() { + return ServerHttpSecurity.this; + } + + protected void configure(ServerHttpSecurity http) { + CorsWebFilter corsFilter = getCorsFilter(); + if (corsFilter != null) { + http.addFilterAt(this.corsFilter, SecurityWebFiltersOrder.CORS); + } + } + + private CorsWebFilter getCorsFilter() { + if (this.corsFilter != null) { + return this.corsFilter; + } + + CorsConfigurationSource source = getBeanOrNull(CorsConfigurationSource.class); + if (source == null) { + return null; + } + CorsProcessor processor = getBeanOrNull(CorsProcessor.class); + if (processor == null) { + processor = new DefaultCorsProcessor(); + } + this.corsFilter = new CorsWebFilter(source, processor); + return this.corsFilter; + } + + private CorsSpec() {} + } + /** * Configures HTTP Basic authentication. An example configuration is provided below: * @@ -361,19 +457,32 @@ public OAuth2LoginSpec authorizedClientService(ReactiveOAuth2AuthorizedClientSer return this; } - protected void configure(LoginPageGeneratingWebFilter loginPageFilter, ServerHttpSecurity http) { - if (loginPageFilter != null) { - loginPageFilter.setOauth2AuthenticationUrlToClientName(getLinks()); - } + /** + * Allows method chaining to continue configuring the {@link ServerHttpSecurity} + * @return the {@link ServerHttpSecurity} to continue configuring + */ + public ServerHttpSecurity and() { + return ServerHttpSecurity.this; + } + + protected void configure(ServerHttpSecurity http) { ReactiveClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository(); ReactiveOAuth2AuthorizedClientService authorizedClientService = getAuthorizedClientService(); OAuth2AuthorizationRequestRedirectWebFilter oauthRedirectFilter = new OAuth2AuthorizationRequestRedirectWebFilter(clientRegistrationRepository); - NimbusReactiveAuthorizationCodeTokenResponseClient client = new NimbusReactiveAuthorizationCodeTokenResponseClient(); + WebClientReactiveAuthorizationCodeTokenResponseClient client = new WebClientReactiveAuthorizationCodeTokenResponseClient(); ReactiveOAuth2UserService userService = new DefaultReactiveOAuth2UserService(); - OAuth2LoginReactiveAuthenticationManager manager = new OAuth2LoginReactiveAuthenticationManager(client, userService, + ReactiveAuthenticationManager manager = new OAuth2LoginReactiveAuthenticationManager(client, userService, authorizedClientService); + + boolean oidcAuthenticationProviderEnabled = ClassUtils.isPresent( + "org.springframework.security.oauth2.jwt.JwtDecoder", this.getClass().getClassLoader()); + if (oidcAuthenticationProviderEnabled) { + OidcAuthorizationCodeReactiveAuthenticationManager oidc = new OidcAuthorizationCodeReactiveAuthenticationManager(client, new OidcReactiveOAuth2UserService(), authorizedClientService); + manager = new DelegatingReactiveAuthenticationManager(oidc, manager); + } + AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter(manager); authenticationFilter.setRequiresAuthenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}")); authenticationFilter.setAuthenticationConverter(new ServerOAuth2LoginAuthenticationTokenConverter(clientRegistrationRepository)); @@ -390,6 +499,16 @@ public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, }); authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository()); + MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher( + MediaType.TEXT_HTML); + htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + Map urlToText = http.oauth2Login.getLinks(); + if (urlToText.size() == 1) { + http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint(urlToText.keySet().iterator().next()))); + } else { + http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint("/login"))); + } + http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC); http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION); } @@ -417,12 +536,149 @@ private ReactiveOAuth2AuthorizedClientService getAuthorizedClientService() { if (this.authorizedClientService == null) { this.authorizedClientService = getBeanOrNull(ReactiveOAuth2AuthorizedClientService.class); } + if (this.authorizedClientService == null) { + this.authorizedClientService = new InMemoryReactiveOAuth2AuthorizedClientService(getClientRegistrationRepository()); + } return this.authorizedClientService; } private OAuth2LoginSpec() {} } + /** + * Configures OAuth2 support. An example configuration is provided below: + * + *
        +	 *  @Bean
        +	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        +	 *      http
        +	 *          // ...
        +	 *          .oauth2()
        +	 *              .resourceServer()
        +	 *                  .jwt()
        +	 *                      .jwkSeturi(jwkSetUri);
        +	 *      return http.build();
        +	 *  }
        +	 * 
        + * + * @return the {@link HttpBasicSpec} to customize + */ + public OAuth2Spec oauth2() { + if (this.oauth2 == null) { + this.oauth2 = new OAuth2Spec(); + } + return this.oauth2; + } + + /** + * Configures OAuth2 Support + * + * @since 5.1 + */ + public class OAuth2Spec { + private ResourceServerSpec resourceServer; + + public ResourceServerSpec resourceServer() { + if (this.resourceServer == null) { + this.resourceServer = new ResourceServerSpec(); + } + return this.resourceServer; + } + + /** + * Configures OAuth2 Resource Server Support + */ + public class ResourceServerSpec { + private JwtSpec jwt; + + public JwtSpec jwt() { + if (this.jwt == null) { + this.jwt = new JwtSpec(); + } + return this.jwt; + } + + protected void configure(ServerHttpSecurity http) { + if (this.jwt != null) { + this.jwt.configure(http); + } + } + + /** + * Configures JWT Resource Server Support + */ + public class JwtSpec { + private ReactiveJwtDecoder jwtDecoder; + + /** + * Configures the {@link ReactiveJwtDecoder} to use + * @param jwtDecoder the decoder to use + * @return the {@code JwtSpec} for additional configuration + */ + public JwtSpec jwtDecoder(ReactiveJwtDecoder jwtDecoder) { + this.jwtDecoder = jwtDecoder; + return this; + } + + /** + * Configures a {@link ReactiveJwtDecoder} that leverages the provided {@link RSAPublicKey} + * + * @param publicKey the public key to use. + * @return the {@code JwtSpec} for additional configuration + */ + public JwtSpec publicKey(RSAPublicKey publicKey) { + this.jwtDecoder = new NimbusReactiveJwtDecoder(publicKey); + return this; + } + + /** + * Configures a {@link ReactiveJwtDecoder} using + * JSON Web Key (JWK) URL + * @param jwkSetUri the URL to use. + * @return the {@code JwtSpec} for additional configuration + */ + public JwtSpec jwkSetUri(String jwkSetUri) { + this.jwtDecoder = new NimbusReactiveJwtDecoder(jwkSetUri); + return this; + } + + public ResourceServerSpec and() { + return ResourceServerSpec.this; + } + + protected void configure(ServerHttpSecurity http) { + BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint(); + JwtReactiveAuthenticationManager authenticationManager = new JwtReactiveAuthenticationManager( + this.jwtDecoder); + AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager); + oauth2.setAuthenticationConverter(new ServerBearerTokenAuthenticationConverter()); + oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint)); + http + .exceptionHandling() + .authenticationEntryPoint(entryPoint) + .and() + .addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION); + } + } + + public OAuth2Spec and() { + return OAuth2Spec.this; + } + } + + public ServerHttpSecurity and() { + return ServerHttpSecurity.this; + } + + protected void configure(ServerHttpSecurity http) { + if (this.resourceServer != null) { + this.resourceServer.configure(http); + } + } + + private OAuth2Spec() {} + } + /** * Configures HTTP Response Headers. The default headers are: * @@ -606,26 +862,27 @@ public SecurityWebFilterChain build() { if(this.csrf != null) { this.csrf.configure(this); } + if (this.cors != null) { + this.cors.configure(this); + } if(this.httpBasic != null) { this.httpBasic.authenticationManager(this.authenticationManager); this.httpBasic.configure(this); } - LoginPageGeneratingWebFilter loginPageFilter = null; if(this.formLogin != null) { this.formLogin.authenticationManager(this.authenticationManager); if(this.securityContextRepository != null) { this.formLogin.securityContextRepository(this.securityContextRepository); } - if(this.formLogin.authenticationEntryPoint == null) { - loginPageFilter = new LoginPageGeneratingWebFilter(); - this.webFilters.add(new OrderedWebFilter(loginPageFilter, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING.getOrder())); - this.webFilters.add(new OrderedWebFilter(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING.getOrder())); - } this.formLogin.configure(this); } if (this.oauth2Login != null) { - this.oauth2Login.configure(loginPageFilter, this); + this.oauth2Login.configure(this); + } + if (this.oauth2 != null) { + this.oauth2.configure(this); } + this.loginPage.configure(this); if(this.logout != null) { this.logout.configure(this); } @@ -638,8 +895,8 @@ public SecurityWebFilterChain build() { exceptionTranslationWebFilter.setAuthenticationEntryPoint( authenticationEntryPoint); } - if(accessDeniedHandler != null) { - exceptionTranslationWebFilter.setAccessDeniedHandler(accessDeniedHandler); + if(this.accessDeniedHandler != null) { + exceptionTranslationWebFilter.setAccessDeniedHandler(this.accessDeniedHandler); } this.addFilterAt(exceptionTranslationWebFilter, SecurityWebFiltersOrder.EXCEPTION_TRANSLATION); this.authorizeExchange.configure(this); @@ -724,7 +981,7 @@ public ServerHttpSecurity and() { /** * Disables authorization. - * @return the {@link ServerHttpSecurity} to continue configuring + * @return the {@link Access} to continue configuring */ @Override public Access anyExchange() { @@ -1075,6 +1332,8 @@ public class FormLoginSpec { private ServerAuthenticationEntryPoint authenticationEntryPoint; + private boolean isEntryPointExplicit; + private ServerWebExchangeMatcher requiresAuthenticationMatcher; private ServerAuthenticationFailureHandler authenticationFailureHandler; @@ -1197,7 +1456,10 @@ public ServerHttpSecurity disable() { protected void configure(ServerHttpSecurity http) { if(this.authenticationEntryPoint == null) { + this.isEntryPointExplicit = false; loginPage("/login"); + } else { + this.isEntryPointExplicit = true; } if(http.requestCache != null) { ServerRequestCache requestCache = http.requestCache.requestCache; @@ -1224,6 +1486,35 @@ private FormLoginSpec() { } } + private class LoginPageSpec { + protected void configure(ServerHttpSecurity http) { + if (http.authenticationEntryPoint != null) { + return; + } + if (http.formLogin != null && http.formLogin.isEntryPointExplicit) { + return; + } + LoginPageGeneratingWebFilter loginPage = null; + if (http.formLogin != null && !http.formLogin.isEntryPointExplicit) { + loginPage = new LoginPageGeneratingWebFilter(); + loginPage.setFormLoginEnabled(true); + } + if (http.oauth2Login != null) { + Map urlToText = http.oauth2Login.getLinks(); + if (loginPage == null) { + loginPage = new LoginPageGeneratingWebFilter(); + } + loginPage.setOauth2AuthenticationUrlToClientName(urlToText); + } + if (loginPage != null) { + http.addFilterAt(loginPage, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING); + http.addFilterAt(new LogoutPageGeneratingWebFilter(), SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING); + } + } + + private LoginPageSpec() {} + } + /** * Configures HTTP Response Headers. * @@ -1367,7 +1658,7 @@ public HeaderSpec and() { /** * Disables frame options response header - * @return the {@link ServerHttpSecurity} to continue configuring + * @return the {@link HeaderSpec} to continue configuring */ public HeaderSpec disable() { HeaderSpec.this.writers.remove(HeaderSpec.this.frameOptions); @@ -1385,17 +1676,21 @@ public class HstsSpec { /** * Configures the max age. Default is one year. * @param maxAge the max age + * @return the {@link HstsSpec} to continue configuring */ - public void maxAge(Duration maxAge) { + public HstsSpec maxAge(Duration maxAge) { HeaderSpec.this.hsts.setMaxAge(maxAge); + return this; } /** * Configures if subdomains should be included. Default is true * @param includeSubDomains if subdomains should be included + * @return the {@link HstsSpec} to continue configuring */ - public void includeSubdomains(boolean includeSubDomains) { + public HstsSpec includeSubdomains(boolean includeSubDomains) { HeaderSpec.this.hsts.setIncludeSubDomains(includeSubDomains); + return this; } /** @@ -1408,7 +1703,7 @@ public HeaderSpec and() { /** * Disables strict transport security response header - * @return the {@link ServerHttpSecurity} to continue configuring + * @return the {@link HeaderSpec} to continue configuring */ public HeaderSpec disable() { HeaderSpec.this.writers.remove(HeaderSpec.this.hsts); @@ -1425,7 +1720,7 @@ private HstsSpec() {} public class XssProtectionSpec { /** * Disables the x-xss-protection response header - * @return + * @return the {@link HeaderSpec} to continue configuring */ public HeaderSpec disable() { HeaderSpec.this.writers.remove(HeaderSpec.this.xss); @@ -1454,7 +1749,7 @@ public final class LogoutSpec { /** * Configures the logout handler. Default is {@code SecurityContextServerLogoutHandler} * @param logoutHandler - * @return + * @return the {@link LogoutSpec} to configure */ public LogoutSpec logoutHandler(ServerLogoutHandler logoutHandler) { this.logoutWebFilter.setLogoutHandler(logoutHandler); diff --git a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java index afa29975af6..8165cca7e35 100644 --- a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java @@ -110,7 +110,7 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements /** * @param element * @param parserContext - * @return + * @return the {@link BeanDefinition} */ public BeanDefinition parse(Element element, ParserContext parserContext) { BeanDefinitionRegistry registry = parserContext.getRegistry(); diff --git a/config/src/main/resources/META-INF/spring.schemas b/config/src/main/resources/META-INF/spring.schemas index 97d4844af25..fe5518e1347 100644 --- a/config/src/main/resources/META-INF/spring.schemas +++ b/config/src/main/resources/META-INF/spring.schemas @@ -1,4 +1,5 @@ -http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.0.xsd +http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.1.xsd +http\://www.springframework.org/schema/security/spring-security-5.1.xsd=org/springframework/security/config/spring-security-5.1.xsd http\://www.springframework.org/schema/security/spring-security-5.0.xsd=org/springframework/security/config/spring-security-5.0.xsd http\://www.springframework.org/schema/security/spring-security-4.2.xsd=org/springframework/security/config/spring-security-4.2.xsd http\://www.springframework.org/schema/security/spring-security-4.1.xsd=org/springframework/security/config/spring-security-4.1.xsd diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc new file mode 100644 index 00000000000..af761497c96 --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.rnc @@ -0,0 +1,916 @@ +namespace a = "http://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:positiveInteger } +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-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}? + + +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? + +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}? + +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? & openid-login? & x509? & jee? & http-basic? & logout? & 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 &= + ## 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 &= + 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 &= + ## 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 "true" + attribute once-per-request {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? + +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 &= + ## The filter list for the path. Currently can be set to "none" to remove a path from having any filters applied. The full filter stack (consisting of all filters created by the namespace configuration, and any added using 'custom-filter'), will be applied to any other paths. + attribute filters {"none"}? +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}? + + +openid-login = + ## Sets up form login for authentication with an Open ID identity + element openid-login {form-login.attlist, user-service-ref?, attribute-exchange*} + +attribute-exchange = + ## Sets up an attribute exchange configuration to request specified attributes from the OpenID identity provider. When multiple elements are used, each must have an identifier-attribute attribute. Each configuration will be matched in turn against the supplied login identifier until a match is found. + element attribute-exchange {attribute-exchange.attlist, openid-attribute+} + +attribute-exchange.attlist &= + ## A regular expression which will be compared against the claimed identity, when deciding which attribute-exchange configuration to use during authentication. + attribute identifier-match {xsd:token}? + +openid-attribute = + ## Attributes used when making an OpenID AX Fetch Request + element openid-attribute {openid-attribute.attlist} + +openid-attribute.attlist &= + ## Specifies the name of the attribute that you wish to get back. For example, email. + attribute name {xsd:token} +openid-attribute.attlist &= + ## Specifies the attribute type. For example, http://axschema.org/contact/email. See your OP's documentation for valid attribute types. + attribute type {xsd:token} +openid-attribute.attlist &= + ## Specifies if this attribute is required to the OP, but does not error out if the OP does not return the attribute. Default is false. + attribute required {xsd:boolean}? +openid-attribute.attlist &= + ## Specifies the number of attributes that you wish to get back. For example, return 3 emails. The default value is 1. + attribute count {xsd:int}? + + +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}? + +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 &= + ## 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:integer}? +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}? + +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 + attribute token-repository-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? & header*)} +headers-options.attlist &= + ## Specifies if the default headers should be disabled. Default false. + attribute defaults-disabled {xsd:boolean}? +headers-options.attlist &= + ## Specifies if headers should be disabled. Default false. + attribute disabled {xsd:boolean}? +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 ammount 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 }? + +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}? + +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'. + 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 that XSS Protection should be explicitly enabled or disabled. Default is 'true' meaning it is enabled. + attribute enabled {xsd:boolean}? +xss-protection.attlist &= + ## Add mode=block to the header or not, default is on. + attribute block {xsd:boolean}? + +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}? + +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" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "CSRF_FILTER" | "LOGOUT_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_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" | "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-5.1.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd new file mode 100644 index 00000000000..e6c0f5a52ac --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.1.xsd @@ -0,0 +1,2738 @@ + + + + + + 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" + + + + + + + + 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. + + + + + + + + + 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 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. + + + + + + + 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 + + + + + + + + + Sets up form login for authentication with an Open ID identity + + + + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + + 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. + + + + + + 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. + + + + + + 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". + + + + + + 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 "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 + + + + + + + + + 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. + + + + + + + + + + + + + + + + + + The filter list for the path. Currently can be set to "none" to remove a path from having + any filters applied. The full filter stack (consisting of all filters created by the + namespace configuration, and any added using 'custom-filter'), will be applied to any + other paths. + + + + + + + + + + + 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 + + + + + + + + Sets up an attribute exchange configuration to request specified attributes from the + OpenID identity provider. When multiple elements are used, each must have an + identifier-attribute attribute. Each configuration will be matched in turn against the + supplied login identifier until a match is found. + + + + + + + + + + + + + A regular expression which will be compared against the claimed identity, when deciding + which attribute-exchange configuration to use during authentication. + + + + + + + Attributes used when making an OpenID AX Fetch Request + + + + + + + + + + Specifies the name of the attribute that you wish to get back. For example, email. + + + + + + Specifies the attribute type. For example, http://axschema.org/contact/email. See your + OP's documentation for valid attribute types. + + + + + + Specifies if this attribute is required to the OP, but does not error out if the OP does + not return the attribute. Default is false. + + + + + + Specifies the number of attributes that you wish to get back. For example, return 3 + emails. The default value is 1. + + + + + + + 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 + + + + + + + + + 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. + + + + + + + + + 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 + + + + + + + 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 ammount 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. + + + + + + + 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 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'. + + + + + + + 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 that XSS Protection should be explicitly enabled or disabled. Default is 'true' + meaning it is enabled. + + + + + + Add mode=block to the header or not, default is on. + + + + + + + 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. + + + + + + + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy index 05ddb22564d..0628529318d 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy @@ -15,7 +15,10 @@ */ package org.springframework.security.config.annotation.web.configurers +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration import org.springframework.security.core.userdetails.PasswordEncodedUser +import org.springframework.security.web.firewall.StrictHttpFirewall import javax.servlet.http.HttpServletResponse @@ -44,7 +47,7 @@ class CsrfConfigurerTests extends BaseSpringSpec { @Unroll def "csrf applied by default"() { setup: - loadConfig(CsrfAppliedDefaultConfig) + loadConfig(CsrfAppliedDefaultConfig, AllowHttpMethodsFirewallConfig) request.method = httpMethod clearCsrfToken() when: @@ -66,11 +69,21 @@ class CsrfConfigurerTests extends BaseSpringSpec { def "csrf default creates CsrfRequestDataValueProcessor"() { when: - loadConfig(CsrfAppliedDefaultConfig) + loadConfig(CsrfAppliedDefaultConfig, AllowHttpMethodsFirewallConfig) then: context.getBean(RequestDataValueProcessor) } + @Configuration + static class AllowHttpMethodsFirewallConfig { + @Bean + StrictHttpFirewall strictHttpFirewall() { + StrictHttpFirewall result = new StrictHttpFirewall(); + result.setAllowedHttpMethods(StrictHttpFirewall.ALLOW_ANY_HTTP_METHOD); + return result; + } + } + @EnableWebSecurity static class CsrfAppliedDefaultConfig extends WebSecurityConfigurerAdapter { diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy index 25f10dbba79..3364d9b104f 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy @@ -53,15 +53,33 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { request.requestURI = "/login" springSecurityFilterChain.doFilter(request,response,chain) then: - response.getContentAsString() == """Login Page -

        Login with Username and Password

        - - - - - -
        User:
        Password:
        -
        """ + response.getContentAsString() == """ + + + + + + + Please sign in + + + + +
        + +""" when: "fail to log in" super.setup() request.servletPath = "/login" @@ -77,15 +95,33 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { request.queryString = "error" springSecurityFilterChain.doFilter(request,response,chain) then: - response.getContentAsString() == """Login Page -

        Your login attempt was not successful, try again.

        Reason: Bad credentials

        Login with Username and Password

        - - - - - -
        User:
        Password:
        -
        """ + response.getContentAsString() == """ + + + + + + + Please sign in + + + + +
        + +""" when: "login success" super.setup() request.servletPath = "/login" @@ -106,15 +142,33 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { request.method = "GET" springSecurityFilterChain.doFilter(request,response,chain) then: "sent to default success page" - response.getContentAsString() == """Login Page -

        You have been logged out

        Login with Username and Password

        - - - - - -
        User:
        Password:
        -
        """ + response.getContentAsString() == """ + + + + + + + Please sign in + + + + +
        + +""" } @Configuration @@ -191,16 +245,34 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { request.requestURI = "/login" springSecurityFilterChain.doFilter(request,response,chain) then: - response.getContentAsString() == """Login Page -

        Login with Username and Password

        - - - - - - -
        User:
        Password:
        Remember me on this computer.
        -
        """ + response.getContentAsString() == """ + + + + + + + Please sign in + + + + +
        + +""" } @Configuration @@ -224,13 +296,29 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { request.requestURI = "/login" springSecurityFilterChain.doFilter(request,response,chain) then: - response.getContentAsString() == """Login Page

        Login with OpenID Identity

        - - - -
        Identity:
        - -
        """ + response.getContentAsString() == """ + + + + + + + Please sign in + + + + +
        + +""" } @Configuration @@ -252,23 +340,44 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { request.requestURI = "/login" springSecurityFilterChain.doFilter(request,response,chain) then: - response.getContentAsString() == """Login Page -

        Login with Username and Password

        - - - - - - -
        User:
        Password:
        Remember me on this computer.
        -

        Login with OpenID Identity

        - - - - -
        Identity:
        Remember me on this computer.
        - -
        """ + response.getContentAsString() == """ + + + + + + + Please sign in + + + + +
        + + +""" } @Configuration diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerSpec.groovy similarity index 99% rename from config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy rename to config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerSpec.groovy index 1f9eeea0d31..09848e960ae 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerSpec.groovy @@ -57,7 +57,7 @@ import spock.lang.Unroll * * @author Rob Winch */ -class FormLoginConfigurerTests extends BaseSpringSpec { +class FormLoginConfigurerSpec extends BaseSpringSpec { def "Form Login"() { when: "load formLogin()" context = new AnnotationConfigApplicationContext(FormLoginConfig) diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy index 07fa2821144..07feb338f12 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -13,6 +13,7 @@ * 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.BeanCreationException @@ -20,14 +21,17 @@ import org.springframework.security.config.annotation.BaseSpringSpec import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter + import static org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy /** + * Tests for {@link HeadersConfigurer}. * * @author Rob Winch * @author Tim Ysewyn * @author Joe Grandja * @author EddĂș MelĂ©ndez + * @author Vedran Pavic */ class HeadersConfigurerTests extends BaseSpringSpec { @@ -497,4 +501,45 @@ class HeadersConfigurerTests extends BaseSpringSpec { } } + def "headers.featurePolicy default header"() { + setup: + loadConfig(FeaturePolicyDefaultConfig) + request.secure = true + when: + springSecurityFilterChain.doFilter(request, response, chain) + then: + responseHeaders == ['Feature-Policy': 'geolocation \'self\''] + } + + @EnableWebSecurity + static class FeaturePolicyDefaultConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .headers() + .defaultsDisabled() + .featurePolicy("geolocation 'self'"); + } + } + + def "headers.featurePolicy empty policyDirectives"() { + when: + loadConfig(FeaturePolicyInvalidConfig) + then: + thrown(BeanCreationException) + } + + @EnableWebSecurity + static class FeaturePolicyInvalidConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .headers() + .defaultsDisabled() + .featurePolicy(""); + } + } + } diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.groovy index 3ace0a8dc50..54d9e16b979 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpFirewallTests.groovy @@ -46,7 +46,7 @@ public class NamespaceHttpFirewallTests extends BaseSpringSpec { MockFilterChain chain def setup() { - request = new MockHttpServletRequest() + request = new MockHttpServletRequest("GET", "") response = new MockHttpServletResponse() chain = new MockFilterChain() } diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpPortMappingsTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpPortMappingsTests.groovy index b36c1501171..2b8dd04b90f 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpPortMappingsTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpPortMappingsTests.groovy @@ -44,7 +44,7 @@ public class NamespaceHttpPortMappingsTests extends BaseSpringSpec { MockFilterChain chain def setup() { - request = new MockHttpServletRequest() + request = new MockHttpServletRequest("GET", "") request.setMethod("GET") response = new MockHttpServletResponse() chain = new MockFilterChain() diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy index fd9a1aecbfb..d73f30c5266 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy @@ -371,7 +371,7 @@ public class NamespaceRememberMeTests extends BaseSpringSpec { } Cookie createRememberMeCookie() { - MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletRequest request = new MockHttpServletRequest("GET", "") MockHttpServletResponse response = new MockHttpServletResponse() super.setupCsrf("CSRF_TOKEN", request, response) diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.groovy index 1f2b8e78b30..18520a98554 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.groovy @@ -270,7 +270,7 @@ public class RememberMeConfigurerTests extends BaseSpringSpec { } Cookie createRememberMeCookie() { - MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletRequest request = new MockHttpServletRequest("GET", "") MockHttpServletResponse response = new MockHttpServletResponse() super.setupCsrf("CSRF_TOKEN", request, response) diff --git a/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy index fc3e73baaa3..6a2ae1c8fcf 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy @@ -28,7 +28,7 @@ import javax.servlet.http.HttpServletRequest * */ abstract class AbstractHttpConfigTests extends AbstractXmlConfigTests { - final int AUTO_CONFIG_FILTERS = 14; + final int AUTO_CONFIG_FILTERS = 15; def httpAutoConfig(Closure c) { xml.http(['auto-config': 'true', 'use-expressions':false], c) @@ -67,7 +67,7 @@ abstract class AbstractHttpConfigTests extends AbstractXmlConfigTests { } FilterInvocation createFilterinvocation(String path, String method) { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.setMethod(method); request.setRequestURI(null); request.setServletPath(path); diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpConfigTests.groovy deleted file mode 100644 index 00545fb508b..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/HttpConfigTests.groovy +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2002-2012 the original author 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 - * - * http://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 static org.mockito.Matchers.any -import static org.mockito.Matchers.eq -import static org.mockito.Mockito.* - -import javax.servlet.http.HttpServletResponse -import javax.servlet.http.HttpServletResponseWrapper - -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse - -/** - * - * @author Rob Winch - */ -class HttpConfigTests extends AbstractHttpConfigTests { - MockHttpServletRequest request = new MockHttpServletRequest('GET','/secure') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - - def 'http minimal configuration works'() { - setup: - xml.http() {} - createAppContext(""" - - """) - when: 'request protected URL' - springSecurityFilterChain.doFilter(request,response,chain) - then: 'sent to login page' - response.status == HttpServletResponse.SC_MOVED_TEMPORARILY - response.redirectedUrl == 'http://localhost/login' - } - - def 'http disable-url-rewriting defaults to true'() { - setup: - xml.http() {} - createAppContext(""" - - """) - HttpServletResponse testResponse = new HttpServletResponseWrapper(response) { - public String encodeURL(String url) { - throw new RuntimeException("Unexpected invocation of encodeURL") - } - public String encodeRedirectURL(String url) { - throw new RuntimeException("Unexpected invocation of encodeURL") - } - public String encodeUrl(String url) { - throw new RuntimeException("Unexpected invocation of encodeURL") - } - public String encodeRedirectUrl(String url) { - throw new RuntimeException("Unexpected invocation of encodeURL") - } - } - when: 'request protected URL' - springSecurityFilterChain.doFilter(request,testResponse,{ request,response-> - response.encodeURL("/url") - response.encodeRedirectURL("/url") - response.encodeUrl("/url") - response.encodeRedirectUrl("/url") - }) - then: 'sent to login page' - response.status == HttpServletResponse.SC_MOVED_TEMPORARILY - response.redirectedUrl == 'http://localhost/login' - } -} \ No newline at end of file diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy deleted file mode 100644 index f18f142173f..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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. You may obtain a copy of the License at - * - * http://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 org.springframework.beans.factory.BeanCreationException - -import javax.servlet.http.HttpServletResponse - -import org.springframework.http.* -import org.springframework.mock.web.* -import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint -import org.springframework.web.bind.annotation.* -import org.springframework.web.filter.CorsFilter -import org.springframework.web.cors.CorsConfiguration -import org.springframework.web.cors.UrlBasedCorsConfigurationSource - -/** - * - * @author Rob Winch - * @author Tim Ysewyn - */ -class HttpCorsConfigTests extends AbstractHttpConfigTests { - MockHttpServletRequest request - MockHttpServletResponse response - MockFilterChain chain - - def setup() { - request = new MockHttpServletRequest(method:"GET") - response = new MockHttpServletResponse() - chain = new MockFilterChain() - } - - def "No MVC throws meaningful error"() { - when: - xml.http('entry-point-ref' : 'ep') { - 'cors'() - 'intercept-url'(pattern:'/**', access: 'authenticated') - } - bean('ep', Http403ForbiddenEntryPoint) - createAppContext() - then: - BeanCreationException success = thrown() - success.message.contains("Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext") - } - - def "HandlerMappingIntrospector explicit"() { - setup: - xml.http('entry-point-ref' : 'ep') { - 'cors'() - 'intercept-url'(pattern:'/**', access: 'authenticated') - } - bean('ep', Http403ForbiddenEntryPoint) - bean('controller', CorsController) - xml.'mvc:annotation-driven'() - createAppContext() - when: - addCors() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'Ensure we a CORS response w/ Spring Security headers too' - responseHeaders['Access-Control-Allow-Origin'] - responseHeaders['X-Content-Type-Options'] - when: - setup() - addCors(true) - springSecurityFilterChain.doFilter(request,response,chain) - then: 'Ensure we a CORS response w/ Spring Security headers too' - responseHeaders['Access-Control-Allow-Origin'] - responseHeaders['X-Content-Type-Options'] - response.status == HttpServletResponse.SC_OK - } - - def "CorsConfigurationSource"() { - setup: - xml.http('entry-point-ref' : 'ep') { - 'cors'('configuration-source-ref':'ccs') - 'intercept-url'(pattern:'/**', access: 'authenticated') - } - bean('ep', Http403ForbiddenEntryPoint) - bean('ccs', MyCorsConfigurationSource) - createAppContext() - when: - addCors() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'Ensure we a CORS response w/ Spring Security headers too' - responseHeaders['Access-Control-Allow-Origin'] - responseHeaders['X-Content-Type-Options'] - when: - setup() - addCors(true) - springSecurityFilterChain.doFilter(request,response,chain) - then: 'Ensure we a CORS response w/ Spring Security headers too' - responseHeaders['Access-Control-Allow-Origin'] - responseHeaders['X-Content-Type-Options'] - response.status == HttpServletResponse.SC_OK - } - - def "CorsFilter"() { - setup: - xml.http('entry-point-ref' : 'ep') { - 'cors'('ref' : 'cf') - 'intercept-url'(pattern:'/**', access: 'authenticated') - } - xml.'b:bean'(id: 'cf', 'class': CorsFilter.name) { - 'b:constructor-arg'(ref: 'ccs') - } - bean('ep', Http403ForbiddenEntryPoint) - bean('ccs', MyCorsConfigurationSource) - createAppContext() - when: - addCors() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'Ensure we a CORS response w/ Spring Security headers too' - responseHeaders['Access-Control-Allow-Origin'] - responseHeaders['X-Content-Type-Options'] - when: - setup() - addCors(true) - springSecurityFilterChain.doFilter(request,response,chain) - then: 'Ensure we a CORS response w/ Spring Security headers too' - responseHeaders['Access-Control-Allow-Origin'] - responseHeaders['X-Content-Type-Options'] - response.status == HttpServletResponse.SC_OK - } - - def addCors(boolean isPreflight=false) { - request.addHeader(HttpHeaders.ORIGIN,"https://example.com") - if(!isPreflight) { - return - } - request.method = HttpMethod.OPTIONS.name() - request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name()) - } - - def getResponseHeaders() { - def headers = [:] - response.headerNames.each { name -> - headers.put(name, response.getHeaderValues(name).join(',')) - } - return headers - } - - @RestController - @CrossOrigin(methods = [ - RequestMethod.GET, RequestMethod.POST - ]) - static class CorsController { - @RequestMapping("/") - String hello() { - "Hello" - } - } - - static class MyCorsConfigurationSource extends UrlBasedCorsConfigurationSource { - MyCorsConfigurationSource() { - registerCorsConfiguration('/**', new CorsConfiguration(allowedOrigins : ['*'], allowedMethods : [ - RequestMethod.GET.name(), - RequestMethod.POST.name() - ])) - } - } -} diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy deleted file mode 100644 index de2a887c864..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy +++ /dev/null @@ -1,961 +0,0 @@ -/* - * 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. You may obtain a copy of the License at - * - * http://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 org.springframework.beans.factory.BeanCreationException -import org.springframework.beans.factory.parsing.BeanDefinitionParsingException -import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.web.FilterChainProxy -import org.springframework.security.web.header.HeaderWriterFilter -import org.springframework.security.web.header.writers.StaticHeadersWriter -import org.springframework.security.web.util.matcher.AnyRequestMatcher - -/** - * - * @author Rob Winch - * @author Tim Ysewyn - */ -class HttpHeadersConfigTests extends AbstractHttpConfigTests { - def defaultHeaders = ['X-Content-Type-Options':'nosniff', - 'X-Frame-Options':'DENY', - 'Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains', - 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', - 'Expires' : '0', - 'Pragma':'no-cache', - 'X-XSS-Protection' : '1; mode=block'] - def 'headers disabled'() { - setup: - httpAutoConfig { - 'headers'(disabled:true) - } - createAppContext() - - when: - def hf = getFilter(HeaderWriterFilter) - then: - !hf - } - - def 'headers disabled with child fails'() { - when: - httpAutoConfig { - 'headers'(disabled:true) { - 'content-type-options'() - } - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - def 'default headers'() { - httpAutoConfig { - } - createAppContext() - - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - then: - assertHeaders(response, defaultHeaders) - } - - def 'http headers with empty headers'() { - setup: - httpAutoConfig { - 'headers'() - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - then: - assertHeaders(response, defaultHeaders) - } - - def 'http headers frame-options@policy=SAMEORIGIN with defaults'() { - httpAutoConfig { - 'headers'() { - 'frame-options'(policy:'SAMEORIGIN') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - def expectedHeaders = [:] << defaultHeaders - expectedHeaders['X-Frame-Options'] = 'SAMEORIGIN' - - expect: - assertHeaders(response, expectedHeaders) - } - - - // --- defaults disabled - - // gh-3986 - def 'http headers defaults-disabled with no override'() { - httpAutoConfig { - 'headers'('defaults-disabled':true) { - } - } - createAppContext() - - expect: - getFilter(HeaderWriterFilter) == null - } - - def 'http headers content-type-options'() { - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'content-type-options'() - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - - expect: - assertHeaders(response, ['X-Content-Type-Options':'nosniff']) - } - - def 'http headers frame-options defaults to DENY'() { - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'() - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - - expect: - assertHeaders(response, ['X-Frame-Options':'DENY']) - } - - def 'http headers frame-options DENY'() { - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'(policy : 'DENY') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - - expect: - assertHeaders(response, ['X-Frame-Options':'DENY']) - } - - def 'http headers frame-options SAMEORIGIN'() { - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'(policy : 'SAMEORIGIN') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - - expect: - assertHeaders(response, ['X-Frame-Options':'SAMEORIGIN']) - } - - def 'http headers frame-options ALLOW-FROM no origin reports error'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'(policy : 'ALLOW-FROM', strategy : 'static') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - - then: - BeanDefinitionParsingException e = thrown() - e.message.contains "Strategy requires a 'value' to be set." // FIME better error message? - } - - def 'http headers frame-options ALLOW-FROM spaces only origin reports error'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'(policy : 'ALLOW-FROM', strategy: 'static', value : ' ') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - - then: - BeanDefinitionParsingException e = thrown() - e.message.contains "Strategy requires a 'value' to be set." // FIME better error message? - } - - def 'http headers frame-options ALLOW-FROM'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'(policy : 'ALLOW-FROM', strategy: 'static', value : 'https://example.com') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - - then: - assertHeaders(response, ['X-Frame-Options':'ALLOW-FROM https://example.com']) - } - - def 'http headers frame-options ALLOW-FROM with whitelist strategy'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'frame-options'(policy : 'ALLOW-FROM', strategy: 'whitelist', value : 'https://example.com') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - - def request = new MockHttpServletRequest() - request.setParameter("from", "https://example.com"); - hf.doFilter(request, response, new MockFilterChain()) - - then: - assertHeaders(response, ['X-Frame-Options':'ALLOW-FROM https://example.com']) - } - - def 'http headers header a=b'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'header'(name : 'a', value: 'b') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - - then: - assertHeaders(response, ['a':'b']) - } - - def 'http headers header a=b and c=d'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'header'(name : 'a', value: 'b') - 'header'(name : 'c', value: 'd') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - - then: - assertHeaders(response , ['a':'b', 'c':'d']) - } - - def 'http headers with ref'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'header'(ref:'headerWriter') - } - } - xml.'b:bean'(id: 'headerWriter', 'class': StaticHeadersWriter.name) { - 'b:constructor-arg'(value:'abc') {} - 'b:constructor-arg'(value:'def') {} - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - then: - assertHeaders(response, ['abc':'def']) - } - - def 'http headers header no name produces error'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'header'(value: 'b') - } - } - createAppContext() - - then: - thrown(BeanCreationException) - } - - def 'http headers header no value produces error'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'header'(name: 'a') - } - } - createAppContext() - - then: - thrown(BeanCreationException) - } - - def 'http headers xss-protection defaults'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'xss-protection'() - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - - then: - assertHeaders(response, ['X-XSS-Protection':'1; mode=block']) - } - - def 'http headers xss-protection enabled=true'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'xss-protection'(enabled:'true') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - - then: - assertHeaders(response, ['X-XSS-Protection':'1; mode=block']) - } - - def 'http headers xss-protection enabled=false'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'xss-protection'(enabled:'false') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - - then: - assertHeaders(response, ['X-XSS-Protection':'0']) - } - - def 'http headers xss-protection enabled=false and block=true produces error'() { - when: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'xss-protection'(enabled:'false', block:'true') - } - } - createAppContext() - - def hf = getFilter(HeaderWriterFilter) - - then: - BeanCreationException e = thrown() - e.message.contains 'Cannot set block to true with enabled false' - } - - def 'http headers cache-control'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'cache-control'() - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - then: - assertHeaders(response, ['Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', - 'Expires' : '0', - 'Pragma':'no-cache']) - } - - def 'http headers hsts'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hsts'() - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - then: - assertHeaders(response, ['Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains']) - } - - def 'http headers hsts default only invokes on HttpServletRequest.isSecure = true'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hsts'() - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - then: - response.headerNames.empty - } - - def 'http headers hsts custom'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hsts'('max-age-seconds':'1','include-subdomains':false, 'request-matcher-ref' : 'matcher') - } - } - - xml.'b:bean'(id: 'matcher', 'class': AnyRequestMatcher.name) - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - then: - assertHeaders(response, ['Strict-Transport-Security': 'max-age=1']) - } - - def 'http headers hpkp no pins'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'() - } - } - when: - createAppContext() - then: - XmlBeanDefinitionStoreException expected = thrown() - expected.message.contains 'The content of element \'hpkp\' is not complete' - } - - def 'http headers hpkp no pin'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'() { - 'pins'() - } - } - } - when: - createAppContext() - then: - XmlBeanDefinitionStoreException expected = thrown() - expected.message.contains 'The content of element \'pins\' is not complete' - } - - def 'http headers hpkp'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'() { - 'pins'() { - 'pin'('algorithm':'sha256', 'd6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - then: - assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="']) - } - - def 'http headers hpkp with default algorithm'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'() { - 'pins'() { - 'pin'('d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - then: - assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="']) - } - - def 'http headers hpkp only invokes on HttpServletRequest.isSecure = true'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'() { - 'pins'() { - 'pin'('algorithm':'sha256', 'E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - then: - response.headerNames.empty - } - - def 'http headers hpkp with custom max age'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'('max-age-seconds':'604800') { - 'pins'() { - 'pin'('algorithm':'sha256', 'd6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - then: - assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=604800 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="']) - } - - def 'http headers hpkp@reportOnly=false'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'('report-only':'false') { - 'pins'() { - 'pin'('algorithm':'sha256', 'E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure: true), response, new MockFilterChain()) - then: - assertHeaders(response, ['Public-Key-Pins': 'max-age=5184000 ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="']) - } - - def 'http headers hpkp@includeSubDomains=true'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'('include-subdomains':'true') { - 'pins'() { - 'pin'('algorithm':'sha256', 'E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure: true), response, new MockFilterChain()) - then: - assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=5184000 ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; includeSubDomains']) - } - - def 'http headers hpkp with report-uri'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'hpkp'('report-uri':'http://example.net/pkp-report') { - 'pins'() { - 'pin'('algorithm':'sha256', 'E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure: true), response, new MockFilterChain()) - then: - assertHeaders(response, ['Public-Key-Pins-Report-Only': 'max-age=5184000 ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; report-uri="http://example.net/pkp-report"']) - } - - // --- disable single default header --- - - def 'http headers cache-controls@disabled=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'cache-control'(disabled:true) - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - def expectedHeaders = [:] << defaultHeaders - expectedHeaders.remove('Cache-Control') - expectedHeaders.remove('Expires') - expectedHeaders.remove('Pragma') - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers content-type-options@disabled=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'content-type-options'(disabled:true) - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - def expectedHeaders = [:] << defaultHeaders - expectedHeaders.remove('X-Content-Type-Options') - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers hsts@disabled=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'hsts'(disabled:true) - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - def expectedHeaders = [:] << defaultHeaders - expectedHeaders.remove('Strict-Transport-Security') - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers hpkp@disabled=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'hpkp'(disabled:true) { - 'pins'() { - 'pin'('algorithm':'sha256', 'E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=') - } - } - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - def expectedHeaders = [:] << defaultHeaders - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers frame-options@disabled=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'frame-options'(disabled:true) - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - def expectedHeaders = [:] << defaultHeaders - expectedHeaders.remove('X-Frame-Options') - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers xss-protection@disabled=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'xss-protection'(disabled:true) - } - } - createAppContext() - def springSecurityFilterChain = appContext.getBean(FilterChainProxy) - MockHttpServletResponse response = new MockHttpServletResponse() - def expectedHeaders = [:] << defaultHeaders - expectedHeaders.remove('X-XSS-Protection') - when: - springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - then: - assertHeaders(response, expectedHeaders) - } - - // --- disable error handling --- - - def 'http headers hsts@disabled=true no include-subdomains'() { - setup: - httpAutoConfig { - 'headers'() { - 'hsts'(disabled:true,'include-subdomains':true) - } - } - when: - createAppContext() - then: - BeanDefinitionParsingException expected = thrown() - expected.message.contains 'include-subdomains' - } - - def 'http headers hsts@disabled=true no max-age'() { - setup: - httpAutoConfig { - 'headers'() { - 'hsts'(disabled:true,'max-age-seconds':123) - } - } - when: - createAppContext() - then: - BeanDefinitionParsingException expected = thrown() - expected.message.contains 'max-age' - } - - def 'http headers hsts@disabled=true no matcher-ref'() { - setup: - httpAutoConfig { - 'headers'() { - 'hsts'(disabled:true,'request-matcher-ref':'matcher') - } - } - xml.'b:bean'(id: 'matcher', 'class': AnyRequestMatcher.name) - when: - createAppContext() - then: - BeanDefinitionParsingException expected = thrown() - expected.message.contains 'request-matcher-ref' - } - - def 'http xss@disabled=true no enabled'() { - setup: - httpAutoConfig { - 'headers'() { - 'xss-protection'(disabled:true,'enabled':true) - } - } - when: - createAppContext() - then: - BeanDefinitionParsingException expected = thrown() - expected.message.contains 'enabled' - } - - def 'http xss@disabled=true no block'() { - setup: - httpAutoConfig { - 'headers'() { - 'xss-protection'(disabled:true,'block':true) - } - } - when: - createAppContext() - then: - BeanDefinitionParsingException expected = thrown() - expected.message.contains 'block' - } - - def 'http frame-options@disabled=true no policy'() { - setup: - httpAutoConfig { - 'headers'() { - 'frame-options'(disabled:true,'policy':'DENY') - } - } - when: - createAppContext() - then: - BeanDefinitionParsingException expected = thrown() - expected.message.contains 'policy' - } - - def 'http headers defaults : content-security-policy'() { - setup: - httpAutoConfig { - 'headers'() { - 'content-security-policy'('policy-directives':'default-src \'self\'') - } - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - def expectedHeaders = [:] << defaultHeaders - expectedHeaders['Content-Security-Policy'] = 'default-src \'self\'' - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers disabled : content-security-policy not included'() { - setup: - httpAutoConfig { - 'headers'(disabled:true) { - 'content-security-policy'('policy-directives':'default-src \'self\'') - } - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - then: - !hf - } - - def 'http headers defaults disabled : content-security-policy only'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'content-security-policy'('policy-directives':'default-src \'self\'') - } - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - then: - assertHeaders(response, ['Content-Security-Policy':'default-src \'self\'']) - } - - def 'http headers defaults : content-security-policy with empty directives'() { - when: - httpAutoConfig { - 'headers'() { - 'content-security-policy'('policy-directives':'') - } - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - def 'http headers defaults : content-security-policy report-only=true'() { - setup: - httpAutoConfig { - 'headers'() { - 'content-security-policy'('policy-directives':'default-src https:; report-uri https://example.com/', 'report-only':true) - } - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain()) - def expectedHeaders = [:] << defaultHeaders - expectedHeaders['Content-Security-Policy-Report-Only'] = 'default-src https:; report-uri https://example.com/' - then: - assertHeaders(response, expectedHeaders) - } - - def 'http headers defaults : referrer-policy'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'referrer-policy'() - } - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - then: - assertHeaders(response, ['Referrer-Policy': 'no-referrer']) - } - - def 'http headers defaults : referrer-policy same-origin'() { - setup: - httpAutoConfig { - 'headers'('defaults-disabled':true) { - 'referrer-policy'('policy': 'same-origin') - } - } - createAppContext() - when: - def hf = getFilter(HeaderWriterFilter) - MockHttpServletResponse response = new MockHttpServletResponse() - hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) - then: - assertHeaders(response, ['Referrer-Policy': 'same-origin']) - } - - def assertHeaders(MockHttpServletResponse response, Map expected) { - assert response.headerNames == expected.keySet() - expected.each { headerName, value -> - assert response.getHeaderValues(headerName) == [value] - } - } -} diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpOpenIDConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpOpenIDConfigTests.groovy deleted file mode 100644 index 1f4c03b8ac0..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/HttpOpenIDConfigTests.groovy +++ /dev/null @@ -1,166 +0,0 @@ -package org.springframework.security.config.http - -import javax.servlet.http.HttpServletRequest -import org.springframework.beans.factory.parsing.BeanDefinitionParsingException -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.config.BeanIds -import org.springframework.security.openid.OpenIDAuthenticationFilter -import org.springframework.security.openid.OpenIDAuthenticationToken -import org.springframework.security.openid.OpenIDConsumer -import org.springframework.security.openid.OpenIDConsumerException - -import org.springframework.security.web.access.ExceptionTranslationFilter -import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices -import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter - -import javax.servlet.Filter - -/** - * - * @author Luke Taylor - */ -class OpenIDConfigTests extends AbstractHttpConfigTests { - - def openIDAndFormLoginWorkTogether() { - xml.http() { - 'openid-login'() - 'form-login'() - } - createAppContext() - - def etf = getFilter(ExceptionTranslationFilter) - def ap = etf.getAuthenticationEntryPoint(); - - expect: - ap.loginFormUrl == "/login" - // Default login filter should be present since we haven't specified any login URLs - getFilter(DefaultLoginPageGeneratingFilter) != null - } - - def formLoginEntryPointTakesPrecedenceIfLoginUrlIsSet() { - xml.http() { - 'openid-login'() - 'form-login'('login-page': '/form-page') - } - createAppContext() - - expect: - getFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl == '/form-page' - } - - def openIDEntryPointTakesPrecedenceIfLoginUrlIsSet() { - xml.http() { - 'openid-login'('login-page': '/openid-page') - 'form-login'() - } - createAppContext() - - expect: - getFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl == '/openid-page' - } - - def multipleLoginPagesCausesError() { - when: - xml.http() { - 'openid-login'('login-page': '/openid-page') - 'form-login'('login-page': '/form-page') - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - def openIDAndRememberMeWorkTogether() { - xml.debug() - xml.http() { - interceptUrl('/**', 'denyAll') - 'openid-login'() - 'remember-me'() - 'csrf'(disabled:true) - } - createAppContext() - - // Default login filter should be present since we haven't specified any login URLs - def loginFilter = getFilter(DefaultLoginPageGeneratingFilter) - def openIDFilter = getFilter(OpenIDAuthenticationFilter) - openIDFilter.setConsumer(new OpenIDConsumer() { - public String beginConsumption(HttpServletRequest req, String claimedIdentity, String returnToUrl, String realm) - throws OpenIDConsumerException { - return "http://testopenid.com?openid.return_to=" + returnToUrl; - } - - public OpenIDAuthenticationToken endConsumption(HttpServletRequest req) throws OpenIDConsumerException { - throw new UnsupportedOperationException(); - } - }) - Set returnToUrlParameters = new HashSet() - returnToUrlParameters.add(AbstractRememberMeServices.DEFAULT_PARAMETER) - openIDFilter.setReturnToUrlParameters(returnToUrlParameters) - assert loginFilter.openIDrememberMeParameter != null - - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET'); - MockHttpServletResponse response = new MockHttpServletResponse(); - - when: "Initial request is made" - Filter fc = appContext.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN) - request.setServletPath("/something.html") - fc.doFilter(request, response, new MockFilterChain()) - then: "Redirected to login" - response.getRedirectedUrl().endsWith("/login") - when: "Login page is requested" - request.setServletPath("/login") - request.setRequestURI("/login") - response = new MockHttpServletResponse() - fc.doFilter(request, response, new MockFilterChain()) - then: "Remember-me choice is added to page" - response.getContentAsString().contains(AbstractRememberMeServices.DEFAULT_PARAMETER) - when: "Login is submitted with remember-me selected" - request.servletPath = "/login/openid" - request.setParameter(OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD, "http://hey.openid.com/") - request.setParameter(AbstractRememberMeServices.DEFAULT_PARAMETER, "on") - response = new MockHttpServletResponse(); - fc.doFilter(request, response, new MockFilterChain()); - String expectedReturnTo = request.getRequestURL().append("?") - .append(AbstractRememberMeServices.DEFAULT_PARAMETER) - .append("=").append("on").toString(); - then: "return_to URL contains remember-me choice" - response.getRedirectedUrl() == "http://testopenid.com?openid.return_to=" + expectedReturnTo - } - - def openIDWithAttributeExchangeConfigurationIsParsedCorrectly() { - xml.http() { - 'openid-login'() { - 'attribute-exchange'() { - 'openid-attribute'(name: 'nickname', type: 'http://schema.openid.net/namePerson/friendly') - 'openid-attribute'(name: 'email', type: 'http://schema.openid.net/contact/email', required: 'true', - 'count': '2') - } - } - } - createAppContext() - - List attributes = getFilter(OpenIDAuthenticationFilter).consumer.attributesToFetchFactory.createAttributeList('http://someid') - - expect: - attributes.size() == 2 - attributes[0].name == 'nickname' - attributes[0].type == 'http://schema.openid.net/namePerson/friendly' - !attributes[0].required - attributes[1].required - attributes[1].getCount() == 2 - } - - def 'SEC-2919: DefaultLoginGeneratingFilter should not be present if login-page="/login"'() { - when: - xml.http() { - 'openid-login'('login-page':'/login') - } - createAppContext() - - then: - getFilter(DefaultLoginPageGeneratingFilter) == null - } - -} diff --git a/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy deleted file mode 100644 index c0f3d1fe09b..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy +++ /dev/null @@ -1,400 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://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 javax.servlet.ServletContext -import javax.servlet.ServletRegistration -import javax.servlet.http.HttpServletResponse - -import org.mockito.invocation.InvocationOnMock -import org.mockito.stubbing.Answer - -import org.springframework.beans.factory.parsing.BeanDefinitionParsingException -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.mock.web.MockServletContext -import org.springframework.security.access.SecurityConfig -import org.springframework.security.web.access.intercept.FilterSecurityInterceptor -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController - -import static org.mockito.Mockito.* -/** - * - * @author Rob Winch - */ -class InterceptUrlConfigTests extends AbstractHttpConfigTests { - - def "SEC-2256: intercept-url method is not given priority"() { - when: - httpAutoConfig { - 'intercept-url'(pattern: '/anyurl', access: "ROLE_USER") - 'intercept-url'(pattern: '/anyurl', 'method':'GET',access: 'ROLE_ADMIN') - } - createAppContext() - - def fids = getFilter(FilterSecurityInterceptor).securityMetadataSource - def attrs = fids.getAttributes(createFilterinvocation("/anyurl", "GET")) - def attrsPost = fids.getAttributes(createFilterinvocation("/anyurl", "POST")) - - then: - attrs.size() == 1 - attrs.contains(new SecurityConfig("ROLE_USER")) - attrsPost.size() == 1 - attrsPost.contains(new SecurityConfig("ROLE_USER")) - } - - def "SEC-2355: intercept-url support patch"() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':false) { - 'http-basic'() - 'intercept-url'(pattern: '/**', 'method':'PATCH',access: 'ROLE_ADMIN') - csrf(disabled:true) - } - createAppContext() - when: 'Method other than PATCH is used' - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_OK - when: 'Method of PATCH is used' - request = new MockHttpServletRequest(method:'PATCH') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - springSecurityFilterChain.doFilter(request, response, chain) - then: 'The response is unauthorized' - response.status == HttpServletResponse.SC_UNAUTHORIZED - } - - def "intercept-url supports hasAnyRoles"() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':true) { - 'http-basic'() - 'intercept-url'(pattern: '/**', access: "hasAnyRole('ROLE_DEVELOPER','ROLE_USER')") - csrf(disabled:true) - } - when: - createAppContext() - then: 'no error' - noExceptionThrown() - when: 'ROLE_USER can access' - login(request, 'user', 'password') - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_OK - when: 'ROLE_A cannot access' - request = new MockHttpServletRequest(method:'GET') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - login(request, 'bob', 'bobspassword') - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is Forbidden' - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "SEC-2256: intercept-url supports path variables"() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':true) { - 'http-basic'() - 'intercept-url'(pattern: '/user/{un}/**', access: "#un == authentication.name") - 'intercept-url'(pattern: '/**', access: "denyAll") - } - createAppContext() - login(request, 'user', 'password') - when: 'user can access' - request.servletPath = '/user/user/abc' - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_OK - when: 'user cannot access otheruser' - request = new MockHttpServletRequest(method:'GET', servletPath : '/user/otheruser/abc') - login(request, 'user', 'password') - response = new MockHttpServletResponse() - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - when: 'user can access case insensitive URL' - request = new MockHttpServletRequest(method:'GET', servletPath : '/USER/user/abc') - login(request, 'user', 'password') - response = new MockHttpServletResponse() - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "gh-3786 intercept-url supports cammel case path variables"() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':true) { - 'http-basic'() - 'intercept-url'(pattern: '/user/{userName}/**', access: "#userName == authentication.name") - 'intercept-url'(pattern: '/**', access: "denyAll") - } - createAppContext() - login(request, 'user', 'password') - when: 'user can access' - request.servletPath = '/user/user/abc' - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_OK - when: 'user cannot access otheruser' - request = new MockHttpServletRequest(method:'GET', servletPath : '/user/otheruser/abc') - login(request, 'user', 'password') - response = new MockHttpServletResponse() - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - when: 'user can access case insensitive URL' - request = new MockHttpServletRequest(method:'GET', servletPath : '/USER/user/abc') - login(request, 'user', 'password') - response = new MockHttpServletResponse() - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "SEC-2256: intercept-url supports path variable type conversion"() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('use-expressions':true) { - 'http-basic'() - 'intercept-url'(pattern: '/user/{un}/**', access: "@id.isOne(#un)") - 'intercept-url'(pattern: '/**', access: "denyAll") - } - bean('id', Id) - createAppContext() - login(request, 'user', 'password') - when: 'can access id == 1' - request.servletPath = '/user/1/abc' - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_OK - when: 'user cannot access 2' - request = new MockHttpServletRequest(method:'GET', servletPath : '/user/2/abc') - login(request, 'user', 'password') - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "intercept-url supports mvc matchers"() { - setup: - MockServletContext servletContext = mockServletContext(); - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('request-matcher':'mvc') { - 'http-basic'() - 'intercept-url'(pattern: '/path', access: "denyAll") - } - bean('pathController',PathController) - xml.'mvc:annotation-driven'() - - createWebAppContext(servletContext) - when: - request.servletPath = "/path" - springSecurityFilterChain.doFilter(request, response, chain) - then: - response.status == HttpServletResponse.SC_UNAUTHORIZED - when: - request = new MockHttpServletRequest(method:'GET') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - request.servletPath = "/path.html" - springSecurityFilterChain.doFilter(request, response, chain) - then: - response.status == HttpServletResponse.SC_UNAUTHORIZED - when: - request = new MockHttpServletRequest(method:'GET') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - request.servletPath = "/path/" - springSecurityFilterChain.doFilter(request, response, chain) - then: - response.status == HttpServletResponse.SC_UNAUTHORIZED - } - - def "intercept-url mvc supports path variables"() { - setup: - MockServletContext servletContext = mockServletContext(); - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('request-matcher':'mvc') { - 'http-basic'() - 'intercept-url'(pattern: '/user/{un}/**', access: "#un == 'user'") - } - xml.'mvc:annotation-driven'() - createWebAppContext(servletContext) - when: 'user can access' - request.servletPath = '/user/user/abc' - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_OK - when: 'cannot access otheruser' - request = new MockHttpServletRequest(method:'GET', servletPath : '/user/otheruser/abc') - login(request, 'user', 'password') - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - when: 'user can access case insensitive URL' - request = new MockHttpServletRequest(method:'GET', servletPath : '/USER/user/abc') - login(request, 'user', 'password') - chain.reset() - springSecurityFilterChain.doFilter(request,response,chain) - then: 'The response is OK' - response.status == HttpServletResponse.SC_FORBIDDEN - } - - def "intercept-url mvc matchers with servlet path"() { - setup: - MockServletContext servletContext = mockServletContext("/spring"); - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - xml.http('request-matcher':'mvc') { - 'http-basic'() - 'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring") - } - bean('pathController',PathController) - xml.'mvc:annotation-driven'() - createWebAppContext(servletContext) - when: - request.servletPath = "/spring" - request.requestURI = "/spring/path" - springSecurityFilterChain.doFilter(request, response, chain) - then: - response.status == HttpServletResponse.SC_UNAUTHORIZED - when: - request = new MockHttpServletRequest(method:'GET') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - request.servletPath = "/spring" - request.requestURI = "/spring/path.html" - springSecurityFilterChain.doFilter(request, response, chain) - then: - response.status == HttpServletResponse.SC_UNAUTHORIZED - when: - request = new MockHttpServletRequest(method:'GET') - response = new MockHttpServletResponse() - chain = new MockFilterChain() - request.servletPath = "/spring" - request.requestURI = "/spring/path/" - springSecurityFilterChain.doFilter(request, response, chain) - then: - response.status == HttpServletResponse.SC_UNAUTHORIZED - } - - def "intercept-url ant matcher with servlet path fails"() { - when: - xml.http('request-matcher':'ant') { - 'http-basic'() - 'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring") - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - def "intercept-url regex matcher with servlet path fails"() { - when: - xml.http('request-matcher':'regex') { - 'http-basic'() - 'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring") - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - def "intercept-url ciRegex matcher with servlet path fails"() { - when: - xml.http('request-matcher':'ciRegex') { - 'http-basic'() - 'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring") - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - def "intercept-url default matcher with servlet path fails"() { - when: - xml.http() { - 'http-basic'() - 'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring") - } - createAppContext() - then: - thrown(BeanDefinitionParsingException) - } - - public static class Id { - public boolean isOne(int i) { - return i == 1; - } - } - - private ServletContext mockServletContext() { - return mockServletContext("/"); - } - - private ServletContext mockServletContext(String servletPath) { - MockServletContext servletContext = spy(new MockServletContext()); - final ServletRegistration registration = mock(ServletRegistration.class); - when(registration.getMappings()).thenReturn(Collections.singleton(servletPath)); - Answer> answer = new Answer>() { - @Override - public Map answer(InvocationOnMock invocation) throws Throwable { - return Collections.singletonMap("spring", registration); - } - }; - when(servletContext.getServletRegistrations()).thenAnswer(answer); - return servletContext; - } - - def login(MockHttpServletRequest request, String username, String password) { - String toEncode = username + ':' + password - request.addHeader('Authorization','Basic ' + Base64.encoder.encodeToString(toEncode.getBytes('UTF-8'))) - } - - @RestController - static class PathController { - @RequestMapping("/path") - public String path() { - return "path"; - } - } -} diff --git a/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy index 4c4bab3b992..eaab25d203b 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy @@ -15,6 +15,7 @@ */ package org.springframework.security.config.http +import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter import org.springframework.security.web.csrf.CsrfFilter import org.springframework.security.web.header.HeaderWriterFilter @@ -113,6 +114,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { Object authProcFilter = filters.next(); assert authProcFilter instanceof UsernamePasswordAuthenticationFilter assert filters.next() instanceof DefaultLoginPageGeneratingFilter + assert filters.next() instanceof DefaultLogoutPageGeneratingFilter assert filters.next() instanceof BasicAuthenticationFilter assert filters.next() instanceof RequestCacheAwareFilter assert filters.next() instanceof SecurityContextHolderAwareRequestFilter @@ -142,7 +144,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { createAppContext() then: Filter debugFilter = appContext.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); - MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletRequest request = new MockHttpServletRequest("GET", "") request.setServletPath("/unprotected"); debugFilter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain()); request.setServletPath("/nomatch"); @@ -189,7 +191,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { createAppContext() expect: - getFilters("/anything")[8] instanceof AnonymousAuthenticationFilter + getFilters("/anything")[9] instanceof AnonymousAuthenticationFilter } def anonymousFilterIsRemovedIfDisabledFlagSet() { @@ -200,7 +202,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { createAppContext() expect: - !(getFilters("/anything").get(5) instanceof AnonymousAuthenticationFilter) + !(getFilters("/anything").get(9) instanceof AnonymousAuthenticationFilter) } def anonymousCustomAttributesAreSetCorrectly() { diff --git a/config/src/test/groovy/org/springframework/security/config/http/MultiHttpBlockConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/MultiHttpBlockConfigTests.groovy deleted file mode 100644 index 6f06c394113..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/MultiHttpBlockConfigTests.groovy +++ /dev/null @@ -1,133 +0,0 @@ -package org.springframework.security.config.http - -import static org.mockito.Mockito.* - -import org.powermock.api.mockito.internal.verification.VerifyNoMoreInteractions; -import org.springframework.beans.factory.parsing.BeanDefinitionParsingException -import org.springframework.mock.web.MockFilterChain; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.config.BeanIds -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.FilterChainProxy -import org.junit.Assert -import org.springframework.beans.factory.BeanCreationException -import org.springframework.security.web.SecurityFilterChain - -/** - * Tests scenarios with multiple <http> elements. - * - * @author Luke Taylor - */ -class MultiHttpBlockConfigTests extends AbstractHttpConfigTests { - - def multipleHttpElementsAreSupported () { - when: "Two elements are used" - xml.http(pattern: '/stateless/**', 'create-session': 'stateless') { - 'http-basic'() - } - xml.http(pattern: '/stateful/**') { - 'form-login'() - } - createAppContext() - FilterChainProxy fcp = appContext.getBean(BeanIds.FILTER_CHAIN_PROXY) - def filterChains = fcp.getFilterChains(); - - then: - filterChains.size() == 2 - filterChains[0].requestMatcher.pattern == '/stateless/**' - } - - def duplicateHttpElementsAreRejected () { - when: "Two elements are used" - xml.http('create-session': 'stateless') { - 'http-basic'() - } - xml.http() { - 'form-login'() - } - createAppContext() - then: - BeanCreationException e = thrown() - e.cause instanceof IllegalArgumentException - } - - def duplicatePatternsAreRejected () { - when: "Two elements with the same pattern are used" - xml.http(pattern: '/stateless/**', 'create-session': 'stateless') { - 'http-basic'() - } - xml.http(pattern: '/stateless/**') { - 'form-login'() - } - createAppContext() - then: - BeanCreationException e = thrown() - e.cause instanceof IllegalArgumentException - } - - - def 'SEC-1937: http@authentication-manager-ref and multi authentication-mananager'() { - setup: - xml.http('authentication-manager-ref' : 'authManager', 'pattern' : '/first/**') { - 'form-login'('login-processing-url': '/first/login') - csrf(disabled:true) - } - xml.http('authentication-manager-ref' : 'authManager2') { - 'form-login'() - csrf(disabled:true) - } - mockBean(UserDetailsService,'uds') - mockBean(UserDetailsService,'uds2') - createAppContext(""" - - - - - - -""") - UserDetailsService uds = appContext.getBean('uds') - UserDetailsService uds2 = appContext.getBean('uds2') - when: - MockHttpServletRequest request = new MockHttpServletRequest() - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - request.servletPath = "/first/login" - request.requestURI = "/first/login" - request.method = 'POST' - springSecurityFilterChain.doFilter(request,response,chain) - then: - verify(uds).loadUserByUsername(anyString()) || true - verifyZeroInteractions(uds2) || true - when: - MockHttpServletRequest request2 = new MockHttpServletRequest() - MockHttpServletResponse response2 = new MockHttpServletResponse() - MockFilterChain chain2 = new MockFilterChain() - request2.servletPath = "/login" - request2.requestURI = "/login" - request2.method = 'POST' - springSecurityFilterChain.doFilter(request2,response2,chain2) - then: - verify(uds2).loadUserByUsername(anyString()) || true - verifyNoMoreInteractions(uds) || true - } - - def multipleAuthenticationManagersWorks () { - xml.http(name: 'basic', pattern: '/basic/**', ) { - 'http-basic'() - } - xml.http(pattern: '/form/**') { - 'form-login'() - } - createAppContext() - FilterChainProxy fcp = appContext.getBean(BeanIds.FILTER_CHAIN_PROXY) - SecurityFilterChain basicChain = fcp.filterChains[0]; - - expect: - Assert.assertSame (basicChain, appContext.getBean('basic')) - } -} diff --git a/config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy deleted file mode 100644 index 13db6f17244..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/PlaceHolderAndELConfigTests.groovy +++ /dev/null @@ -1,156 +0,0 @@ -package org.springframework.security.config.http - -import java.text.MessageFormat - -import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.access.SecurityConfig -import org.springframework.security.config.BeanIds -import org.springframework.security.util.FieldUtils -import org.springframework.security.web.PortMapperImpl -import org.springframework.security.web.access.ExceptionTranslationFilter -import org.springframework.security.web.access.channel.ChannelProcessingFilter -import org.springframework.security.web.access.intercept.FilterSecurityInterceptor -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter - -class PlaceHolderAndELConfigTests extends AbstractHttpConfigTests { - - def setup() { - // Add a PropertyPlaceholderConfigurer to the context for all the tests - bean(PropertyPlaceholderConfigurer.class.name, PropertyPlaceholderConfigurer.class) - } - - def unsecuredPatternSupportsPlaceholderForPattern() { - System.setProperty("pattern.nofilters", "/unprotected"); - - xml.http(pattern: '${pattern.nofilters}', security: 'none') - httpAutoConfig() { - interceptUrl('/**', 'ROLE_A') - } - createAppContext() - - List filters = getFilters("/unprotected"); - - expect: - filters.size() == 0 - } - - // SEC-1201 - def interceptUrlsAndFormLoginSupportPropertyPlaceholders() { - System.setProperty("secure.Url", "/secure"); - System.setProperty("secure.role", "ROLE_A"); - System.setProperty("login.page", "/loginPage"); - System.setProperty("default.target", "/defaultTarget"); - System.setProperty("auth.failure", "/authFailure"); - - xml.http(pattern: '${login.page}', security: 'none') - xml.http('use-expressions':false) { - interceptUrl('${secure.Url}', '${secure.role}') - 'form-login'('login-page':'${login.page}', 'default-target-url': '${default.target}', - 'authentication-failure-url':'${auth.failure}'); - } - createAppContext(); - - expect: - propertyValuesMatchPlaceholders() - getFilters("/loginPage").size() == 0 - } - - // SEC-1309 - def interceptUrlsAndFormLoginSupportEL() { - System.setProperty("secure.url", "/secure"); - System.setProperty("secure.role", "ROLE_A"); - System.setProperty("login.page", "/loginPage"); - System.setProperty("default.target", "/defaultTarget"); - System.setProperty("auth.failure", "/authFailure"); - - xml.http('use-expressions':false) { - interceptUrl("#{systemProperties['secure.url']}", "#{systemProperties['secure.role']}") - 'form-login'('login-page':"#{systemProperties['login.page']}", 'default-target-url': "#{systemProperties['default.target']}", - 'authentication-failure-url':"#{systemProperties['auth.failure']}"); - } - createAppContext() - - expect: - propertyValuesMatchPlaceholders() - } - - private void propertyValuesMatchPlaceholders() { - // Check the security attribute - def fis = getFilter(FilterSecurityInterceptor); - def fids = fis.getSecurityMetadataSource(); - Collection attrs = fids.getAttributes(createFilterinvocation("/secure", null)); - assert attrs.size() == 1 - assert attrs.contains(new SecurityConfig("ROLE_A")) - - // Check the form login properties are set - def apf = getFilter(UsernamePasswordAuthenticationFilter) - assert FieldUtils.getFieldValue(apf, "successHandler.defaultTargetUrl") == '/defaultTarget' - assert "/authFailure" == FieldUtils.getFieldValue(apf, "failureHandler.defaultFailureUrl") - - def etf = getFilter(ExceptionTranslationFilter) - assert "/loginPage"== etf.authenticationEntryPoint.loginFormUrl - } - - def portMappingsWorkWithPlaceholdersAndEL() { - System.setProperty("http", "9080"); - System.setProperty("https", "9443"); - - httpAutoConfig { - 'port-mappings'() { - 'port-mapping'(http: '#{systemProperties.http}', https: '${https}') - } - } - createAppContext(); - - def pm = (appContext.getBeansOfType(PortMapperImpl).values() as List)[0]; - - expect: - pm.getTranslatedPortMappings().size() == 1 - pm.lookupHttpPort(9443) == 9080 - pm.lookupHttpsPort(9080) == 9443 - } - - def requiresChannelSupportsPlaceholder() { - System.setProperty("secure.url", "/secure"); - System.setProperty("required.channel", "https"); - - httpAutoConfig { - 'intercept-url'(pattern: '${secure.url}', 'requires-channel': '${required.channel}') - } - createAppContext(); - List filters = getFilters("/secure"); - - expect: - filters.size() == AUTO_CONFIG_FILTERS + 1 - filters[0] instanceof ChannelProcessingFilter - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setServletPath("/secure"); - MockHttpServletResponse response = new MockHttpServletResponse(); - filters[0].doFilter(request, response, new MockFilterChain()); - response.getRedirectedUrl().startsWith("https") - } - - def accessDeniedPageWorksWithPlaceholders() { - System.setProperty("accessDenied", "/go-away"); - xml.http('auto-config': 'true') { - 'access-denied-handler'('error-page' : '${accessDenied}') {} - } - createAppContext(); - - expect: - FieldUtils.getFieldValue(getFilter(ExceptionTranslationFilter.class), "accessDeniedHandler.errorPage") == '/go-away' - } - - def accessDeniedHandlerPageWorksWithEL() { - httpAutoConfig { - 'access-denied-handler'('error-page': "#{'/go' + '-away'}") - } - createAppContext() - - expect: - getFilter(ExceptionTranslationFilter).accessDeniedHandler.errorPage == '/go-away' - } -} diff --git a/config/src/test/groovy/org/springframework/security/config/http/RememberMeConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/RememberMeConfigTests.groovy deleted file mode 100644 index 7217cd11662..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/RememberMeConfigTests.groovy +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright 2002-2015 the original author 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 - * - * http://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 static org.springframework.security.config.ConfigTestUtils.AUTH_PROVIDER_XML - -import javax.sql.DataSource - -import org.springframework.beans.FatalBeanException -import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer -import org.springframework.beans.factory.parsing.BeanDefinitionParsingException -import org.springframework.security.TestDataSource -import org.springframework.security.authentication.ProviderManager -import org.springframework.security.authentication.RememberMeAuthenticationProvider -import org.springframework.security.core.userdetails.MockUserDetailsService -import org.springframework.security.util.FieldUtils -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler -import org.springframework.security.web.authentication.logout.LogoutFilter -import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler -import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices -import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl -import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl -import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices -import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter -import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices - -/** - * - * @author Luke Taylor - * @author Rob Winch - * @author Oliver Becker - */ -class RememberMeConfigTests extends AbstractHttpConfigTests { - - def rememberMeServiceWorksWithTokenRepoRef() { - httpAutoConfig () { - 'remember-me'('token-repository-ref': 'tokenRepo') - } - bean('tokenRepo', CustomTokenRepository.class.name) - - createAppContext(AUTH_PROVIDER_XML) - - def rememberMeServices = rememberMeServices() - - expect: - rememberMeServices instanceof PersistentTokenBasedRememberMeServices - rememberMeServices.tokenRepository instanceof CustomTokenRepository - FieldUtils.getFieldValue(rememberMeServices, "useSecureCookie") == null - } - - def rememberMeServiceWorksWithDataSourceRef() { - httpAutoConfig () { - 'remember-me'('data-source-ref': 'ds') - } - bean('ds', TestDataSource.class.name, ['tokendb']) - - createAppContext(AUTH_PROVIDER_XML) - - def rememberMeServices = rememberMeServices() - - expect: - rememberMeServices instanceof PersistentTokenBasedRememberMeServices - rememberMeServices.tokenRepository instanceof JdbcTokenRepositoryImpl - } - - def rememberMeServiceWorksWithAuthenticationSuccessHandlerRef() { - httpAutoConfig () { - 'remember-me'('authentication-success-handler-ref': 'sh') - } - bean('sh', SimpleUrlAuthenticationSuccessHandler.class.name, ['/target']) - - createAppContext(AUTH_PROVIDER_XML) - - expect: - getFilter(RememberMeAuthenticationFilter.class).successHandler instanceof SimpleUrlAuthenticationSuccessHandler - } - - def rememberMeServiceWorksWithExternalServicesImpl() { - httpAutoConfig () { - 'remember-me'('key': "#{'our' + 'key'}", 'services-ref': 'rms') - csrf(disabled:true) - } - xml.'b:bean'(id: 'rms', 'class': TokenBasedRememberMeServices.class.name) { - 'b:constructor-arg'(value: 'ourKey') - 'b:constructor-arg'(ref: 'us') - 'b:property'(name: 'tokenValiditySeconds', value: '5000') - } - - createAppContext(AUTH_PROVIDER_XML) - - List logoutHandlers = FieldUtils.getFieldValue(getFilter(LogoutFilter.class), "handler").logoutHandlers; - Map ams = appContext.getBeansOfType(ProviderManager.class); - ProviderManager am = (ams.values() as List).find { it instanceof ProviderManager && it.providers.size() == 2} - RememberMeAuthenticationProvider rmp = am.providers.find { it instanceof RememberMeAuthenticationProvider} - - expect: - rmp != null - 5000 == FieldUtils.getFieldValue(rememberMeServices(), "tokenValiditySeconds") - // SEC-909 - logoutHandlers.size() == 2 - logoutHandlers.get(1) == rememberMeServices() - // SEC-1281 - rmp.key == "ourkey" - } - - def rememberMeAddsLogoutHandlerToLogoutFilter() { - httpAutoConfig () { - 'remember-me'() - csrf(disabled:true) - } - createAppContext(AUTH_PROVIDER_XML) - - def rememberMeServices = rememberMeServices() - List logoutHandlers = getFilter(LogoutFilter.class).handler.logoutHandlers - - expect: - rememberMeServices - logoutHandlers.size() == 2 - logoutHandlers.get(0) instanceof SecurityContextLogoutHandler - logoutHandlers.get(1) == rememberMeServices - } - - def rememberMeTokenValidityIsParsedCorrectly() { - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'token-validity-seconds':'10000') - } - - createAppContext(AUTH_PROVIDER_XML) - - def rememberMeServices = rememberMeServices() - def rememberMeFilter = getFilter(RememberMeAuthenticationFilter.class) - - expect: - rememberMeFilter.authenticationManager - rememberMeServices.key == 'ourkey' - rememberMeServices.tokenValiditySeconds == 10000 - rememberMeServices.userDetailsService - } - - def 'Remember-me token validity allows negative value for non-persistent implementation'() { - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'token-validity-seconds':'-1') - } - - createAppContext(AUTH_PROVIDER_XML) - expect: - rememberMeServices().tokenValiditySeconds == -1 - } - - def 'remember-me@token-validity-seconds denies for persistent implementation'() { - setup: - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'token-validity-seconds':'-1', 'dataSource' : 'dataSource') - } - mockBean(DataSource) - when: - createAppContext(AUTH_PROVIDER_XML) - then: - thrown(FatalBeanException) - } - - def 'SEC-2165: remember-me@token-validity-seconds allows property placeholders'() { - when: - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'token-validity-seconds':'${security.rememberme.ttl}') - } - xml.'b:bean'(class: PropertyPlaceholderConfigurer.name) { - 'b:property'(name:'properties', value:'security.rememberme.ttl=30') - } - - createAppContext(AUTH_PROVIDER_XML) - then: - rememberMeServices().tokenValiditySeconds == 30 - } - - def rememberMeSecureCookieAttributeIsSetCorrectly() { - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'use-secure-cookie':'true') - } - - createAppContext(AUTH_PROVIDER_XML) - expect: - FieldUtils.getFieldValue(rememberMeServices(), "useSecureCookie") - } - - // SEC-1827 - def rememberMeSecureCookieAttributeFalse() { - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'use-secure-cookie':'false') - } - - createAppContext(AUTH_PROVIDER_XML) - expect: 'useSecureCookie is false' - FieldUtils.getFieldValue(rememberMeServices(), "useSecureCookie") == Boolean.FALSE - } - - def 'Negative token-validity is rejected with persistent implementation'() { - when: - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'token-validity-seconds':'-1', 'token-repository-ref': 'tokenRepo') - } - bean('tokenRepo', InMemoryTokenRepositoryImpl.class.name) - createAppContext(AUTH_PROVIDER_XML) - - then: - BeanDefinitionParsingException e = thrown() - } - - def 'Custom user service is supported'() { - when: - httpAutoConfig () { - 'remember-me'('key': 'ourkey', 'token-validity-seconds':'-1', 'user-service-ref': 'userService') - } - bean('userService', MockUserDetailsService.class.name) - createAppContext(AUTH_PROVIDER_XML) - - then: "Parses OK" - notThrown BeanDefinitionParsingException - } - - // SEC-742 - def rememberMeWorksWithoutBasicProcessingFilter() { - when: - xml.http () { - 'form-login'('login-page': '/login.jsp', 'default-target-url': '/messageList.html' ) - logout('logout-success-url': '/login.jsp') - anonymous(username: 'guest', 'granted-authority': 'guest') - 'remember-me'() - } - createAppContext(AUTH_PROVIDER_XML) - - then: "Parses OK" - notThrown BeanDefinitionParsingException - } - - def 'Default remember-me-parameter is correct'() { - httpAutoConfig () { - 'remember-me'() - } - - createAppContext(AUTH_PROVIDER_XML) - expect: - rememberMeServices().parameter == AbstractRememberMeServices.DEFAULT_PARAMETER - } - - // SEC-2119 - def 'Custom remember-me-parameter is supported'() { - httpAutoConfig () { - 'remember-me'('remember-me-parameter': 'ourParam') - } - - createAppContext(AUTH_PROVIDER_XML) - expect: - rememberMeServices().parameter == 'ourParam' - } - - def 'remember-me-parameter cannot be used together with services-ref'() { - when: - httpAutoConfig () { - 'remember-me'('remember-me-parameter': 'ourParam', 'services-ref': 'ourService') - } - createAppContext(AUTH_PROVIDER_XML) - then: - BeanDefinitionParsingException e = thrown() - } - - // SEC-2826 - def 'Custom remember-me-cookie is supported'() { - httpAutoConfig () { - 'remember-me'('remember-me-cookie': 'ourCookie') - } - - createAppContext(AUTH_PROVIDER_XML) - expect: - rememberMeServices().cookieName == 'ourCookie' - } - - // SEC-2826 - def 'remember-me-cookie cannot be used together with services-ref'() { - when: - httpAutoConfig () { - 'remember-me'('remember-me-cookie': 'ourCookie', 'services-ref': 'ourService') - } - - createAppContext(AUTH_PROVIDER_XML) - then: - BeanDefinitionParsingException e = thrown() - expect: - e.message == 'Configuration problem: services-ref can\'t be used in combination with attributes token-repository-ref,data-source-ref, user-service-ref, token-validity-seconds, use-secure-cookie, remember-me-parameter or remember-me-cookie\nOffending resource: null' - } - - def rememberMeServices() { - getFilter(RememberMeAuthenticationFilter.class).getRememberMeServices() - } - - static class CustomTokenRepository extends InMemoryTokenRepositoryImpl { - - } -} diff --git a/config/src/test/groovy/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.groovy deleted file mode 100644 index de68710f253..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.groovy +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2002-2012 the original author 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 - * - * http://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 static org.springframework.security.config.ConfigTestUtils.AUTH_PROVIDER_XML - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.authentication.TestingAuthenticationToken -import org.springframework.security.core.context.SecurityContext -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.web.access.ExceptionTranslationFilter -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter -import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler -import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler -import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter -import org.springframework.security.web.context.HttpSessionSecurityContextRepository -import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter - -/** - * - * @author Rob Winch - */ -class SecurityContextHolderAwareRequestConfigTests extends AbstractHttpConfigTests { - - def withAutoConfig() { - httpAutoConfig () { - csrf(disabled:true) - } - createAppContext(AUTH_PROVIDER_XML) - - def securityContextAwareFilter = getFilter(SecurityContextHolderAwareRequestFilter) - - expect: - securityContextAwareFilter.authenticationEntryPoint.loginFormUrl == getFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl - securityContextAwareFilter.authenticationManager == getFilter(UsernamePasswordAuthenticationFilter).authenticationManager - securityContextAwareFilter.logoutHandlers.size() == 1 - securityContextAwareFilter.logoutHandlers[0].class == SecurityContextLogoutHandler - } - - def explicitEntryPoint() { - xml.http() { - 'http-basic'('entry-point-ref': 'ep') - } - bean('ep', BasicAuthenticationEntryPoint.class.name, ['realmName':'whocares'],[:]) - createAppContext(AUTH_PROVIDER_XML) - - def securityContextAwareFilter = getFilter(SecurityContextHolderAwareRequestFilter) - - expect: - securityContextAwareFilter.authenticationEntryPoint == getFilter(ExceptionTranslationFilter).authenticationEntryPoint - securityContextAwareFilter.authenticationManager == getFilter(BasicAuthenticationFilter).authenticationManager - securityContextAwareFilter.logoutHandlers == null - } - - def formLogin() { - xml.http() { - 'form-login'() - } - createAppContext(AUTH_PROVIDER_XML) - - def securityContextAwareFilter = getFilter(SecurityContextHolderAwareRequestFilter) - - expect: - securityContextAwareFilter.authenticationEntryPoint.loginFormUrl == getFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl - securityContextAwareFilter.authenticationManager == getFilter(UsernamePasswordAuthenticationFilter).authenticationManager - securityContextAwareFilter.logoutHandlers == null - } - - def multiHttp() { - xml.http('authentication-manager-ref' : 'authManager', 'pattern' : '/first/**') { - 'form-login'('login-page' : '/login') - 'logout'('invalidate-session' : 'true') - csrf(disabled:true) - } - xml.http('authentication-manager-ref' : 'authManager2') { - 'form-login'('login-page' : '/login2') - 'logout'('invalidate-session' : 'false') - csrf(disabled:true) - } - - String secondAuthManager = AUTH_PROVIDER_XML.replace("alias='authManager'", "id='authManager2'") - createAppContext(AUTH_PROVIDER_XML + secondAuthManager) - - def securityContextAwareFilter = getFilters('/first/filters').find { it instanceof SecurityContextHolderAwareRequestFilter } - def secondSecurityContextAwareFilter = getFilter(SecurityContextHolderAwareRequestFilter) - - expect: - securityContextAwareFilter.authenticationEntryPoint.loginFormUrl == '/login' - securityContextAwareFilter.authenticationManager == getFilters('/first/filters').find { it instanceof UsernamePasswordAuthenticationFilter}.authenticationManager - securityContextAwareFilter.authenticationManager.parent == appContext.getBean('authManager') - securityContextAwareFilter.logoutHandlers.size() == 1 - securityContextAwareFilter.logoutHandlers[0].class == SecurityContextLogoutHandler - securityContextAwareFilter.logoutHandlers[0].invalidateHttpSession == true - - secondSecurityContextAwareFilter.authenticationEntryPoint.loginFormUrl == '/login2' - secondSecurityContextAwareFilter.authenticationManager == getFilter(UsernamePasswordAuthenticationFilter).authenticationManager - secondSecurityContextAwareFilter.authenticationManager.parent == appContext.getBean('authManager2') - securityContextAwareFilter.logoutHandlers.size() == 1 - secondSecurityContextAwareFilter.logoutHandlers[0].class == SecurityContextLogoutHandler - secondSecurityContextAwareFilter.logoutHandlers[0].invalidateHttpSession == false - } - - def logoutCustom() { - xml.http() { - 'form-login'('login-page' : '/login') - 'logout'('invalidate-session' : 'false', 'logout-success-url' : '/login?logout', 'delete-cookies' : 'JSESSIONID') - csrf(disabled:true) - } - createAppContext(AUTH_PROVIDER_XML) - - def securityContextAwareFilter = getFilter(SecurityContextHolderAwareRequestFilter) - - expect: - securityContextAwareFilter.authenticationEntryPoint.loginFormUrl == getFilter(ExceptionTranslationFilter).authenticationEntryPoint.loginFormUrl - securityContextAwareFilter.authenticationManager == getFilter(UsernamePasswordAuthenticationFilter).authenticationManager - securityContextAwareFilter.logoutHandlers.size() == 2 - securityContextAwareFilter.logoutHandlers[0].class == SecurityContextLogoutHandler - securityContextAwareFilter.logoutHandlers[0].invalidateHttpSession == false - securityContextAwareFilter.logoutHandlers[1].class == CookieClearingLogoutHandler - securityContextAwareFilter.logoutHandlers[1].cookiesToClear == ['JSESSIONID'] - } - - def 'SEC-2926: Role Prefix is set'() { - setup: - httpAutoConfig () { - - } - createAppContext(AUTH_PROVIDER_XML) - - MockFilterChain chain = new MockFilterChain() { - public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { - assert request.isUserInRole("USER") - - super.doFilter(request,response) - } - } - MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') - SecurityContext context = SecurityContextHolder.createEmptyContext() - context.setAuthentication(new TestingAuthenticationToken("user", "pass", "ROLE_USER")) - request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context) - - when: - springSecurityFilterChain.doFilter(request, new MockHttpServletResponse(), chain) - then: - chain.request != null - } -} diff --git a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy deleted file mode 100644 index a4e2e6748ca..00000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy +++ /dev/null @@ -1,430 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://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 org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -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.session.SessionRegistry -import org.springframework.security.core.session.SessionRegistryImpl -import org.springframework.security.core.userdetails.User -import org.springframework.security.util.FieldUtils -import org.springframework.security.web.FilterChainProxy -import org.springframework.security.web.authentication.RememberMeServices -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter -import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler -import org.springframework.security.web.authentication.logout.LogoutFilter -import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler -import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter -import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy -import org.springframework.security.web.context.NullSecurityContextRepository -import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper -import org.springframework.security.web.context.SecurityContextPersistenceFilter -import org.springframework.security.web.savedrequest.RequestCacheAwareFilter -import org.springframework.security.web.session.ConcurrentSessionFilter -import org.springframework.security.web.session.SessionManagementFilter - -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -import static org.junit.Assert.assertSame -import static org.mockito.Matchers.any -import static org.mockito.Mockito.verify - -/** - * Tests session-related functionality for the <http> namespace element and <session-management> - * - * @author Luke Taylor - * @author Rob Winch - */ -class SessionManagementConfigTests extends AbstractHttpConfigTests { - - def settingCreateSessionToAlwaysSetsFilterPropertiesCorrectly() { - httpCreateSession('always') { } - createAppContext(); - - def filter = getFilter(SecurityContextPersistenceFilter.class); - - expect: - filter.forceEagerSessionCreation - filter.repo.allowSessionCreation - filter.repo.disableUrlRewriting - } - - def settingCreateSessionToNeverSetsFilterPropertiesCorrectly() { - httpCreateSession('never') { } - createAppContext(); - - def filter = getFilter(SecurityContextPersistenceFilter.class); - - expect: - !filter.forceEagerSessionCreation - !filter.repo.allowSessionCreation - } - - def settingCreateSessionToStatelessSetsFilterPropertiesCorrectly() { - httpCreateSession('stateless') { } - createAppContext(); - - def filter = getFilter(SecurityContextPersistenceFilter.class); - - expect: - !filter.forceEagerSessionCreation - filter.repo instanceof NullSecurityContextRepository - getFilter(SessionManagementFilter.class) == null - getFilter(RequestCacheAwareFilter.class) == null - } - - def settingCreateSessionToIfRequiredDoesntCreateASessionForPublicInvocation() { - httpCreateSession('ifRequired') { } - createAppContext(); - - def filter = getFilter(SecurityContextPersistenceFilter.class); - - expect: - !filter.forceEagerSessionCreation - filter.repo.allowSessionCreation - } - - def 'SEC-1208: Session is not created when rejecting user due to max sessions exceeded'() { - setup: - httpCreateSession('never') { - 'session-management'() { - 'concurrency-control'('max-sessions':1,'error-if-maximum-exceeded':'true') - } - csrf(disabled:true) - } - createAppContext() - SessionRegistry registry = appContext.getBean(SessionRegistry) - registry.registerNewSession("1", new User("user","password",AuthorityUtils.createAuthorityList("ROLE_USER"))) - MockHttpServletRequest request = new MockHttpServletRequest() - MockHttpServletResponse response = new MockHttpServletResponse() - String credentials = "user:password" - request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64()) - when: "exceed max authentication attempts" - appContext.getBean(FilterChainProxy).doFilter(request, response, new MockFilterChain()) - then: "no new session is created" - request.getSession(false) == null - response.status == HttpServletResponse.SC_UNAUTHORIZED - } - - def 'SEC-2137: disable session fixation and enable concurrency control'() { - setup: "context where session fixation is disabled and concurrency control is enabled" - httpAutoConfig { - 'session-management'('session-fixation-protection':'none') { - 'concurrency-control'('max-sessions':'1','error-if-maximum-exceeded':'true') - } - } - createAppContext() - MockHttpServletRequest request = new MockHttpServletRequest() - MockHttpServletResponse response = new MockHttpServletResponse() - String originalSessionId = request.session.id - String credentials = "user:password" - request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64()) - when: "authenticate" - appContext.getBean(FilterChainProxy).doFilter(request, response, new MockFilterChain()) - then: "session invalidate is not called" - request.session.id == originalSessionId - } - - def httpCreateSession(String create, Closure c) { - xml.http(['auto-config': 'true', 'create-session': create], c) - } - - def concurrentSessionSupportAddsFilterAndExpectedBeans() { - when: - httpAutoConfig { - 'session-management'() { - 'concurrency-control'('session-registry-alias':'sr', 'expired-url': '/expired') - } - csrf(disabled:true) - } - createAppContext(); - List filters = getFilters("/someurl"); - def concurrentSessionFilter = filters.get(1) - - then: - concurrentSessionFilter instanceof ConcurrentSessionFilter - concurrentSessionFilter.sessionInformationExpiredStrategy.destinationUrl == '/expired' - appContext.getBean("sr") != null - getFilter(SessionManagementFilter.class) != null - sessionRegistryIsValid(); - - concurrentSessionFilter.handlers.logoutHandlers.size() == 1 - def logoutHandler = concurrentSessionFilter.handlers.logoutHandlers[0] - logoutHandler instanceof SecurityContextLogoutHandler - logoutHandler.invalidateHttpSession - - } - - def 'concurrency-control adds custom logout handlers'() { - when: 'Custom logout and remember-me' - httpAutoConfig { - 'session-management'() { - 'concurrency-control'() - } - 'logout'('invalidate-session': false, 'delete-cookies': 'testCookie') - 'remember-me'() - csrf(disabled:true) - } - createAppContext() - - List filters = getFilters("/someurl") - ConcurrentSessionFilter concurrentSessionFilter = filters.get(1) - def logoutHandlers = concurrentSessionFilter.handlers.logoutHandlers - - then: 'ConcurrentSessionFilter contains the customized LogoutHandlers' - logoutHandlers.size() == 3 - def securityCtxlogoutHandler = logoutHandlers.find { it instanceof SecurityContextLogoutHandler } - securityCtxlogoutHandler.invalidateHttpSession == false - def cookieClearingLogoutHandler = logoutHandlers.find { it instanceof CookieClearingLogoutHandler } - cookieClearingLogoutHandler.cookiesToClear == ['testCookie'] - def remembermeLogoutHandler = logoutHandlers.find { it instanceof RememberMeServices } - remembermeLogoutHandler == getFilter(RememberMeAuthenticationFilter.class).rememberMeServices - } - - def 'concurrency-control with remember-me and no LogoutFilter contains SecurityContextLogoutHandler and RememberMeServices as LogoutHandlers'() { - when: 'RememberMe and No LogoutFilter' - xml.http(['entry-point-ref': 'entryPoint'], { - 'session-management'() { - 'concurrency-control'() - } - 'remember-me'() - csrf(disabled:true) - }) - bean('entryPoint', 'org.springframework.security.web.authentication.Http403ForbiddenEntryPoint') - createAppContext() - - List filters = getFilters("/someurl") - ConcurrentSessionFilter concurrentSessionFilter = filters.get(1) - def logoutHandlers = concurrentSessionFilter.handlers.logoutHandlers - - then: 'SecurityContextLogoutHandler and RememberMeServices are in ConcurrentSessionFilter logoutHandlers' - !filters.find { it instanceof LogoutFilter } - logoutHandlers.size() == 2 - def securityCtxlogoutHandler = logoutHandlers.find { it instanceof SecurityContextLogoutHandler } - securityCtxlogoutHandler.invalidateHttpSession == true - logoutHandlers.find { it instanceof RememberMeServices } == getFilter(RememberMeAuthenticationFilter).rememberMeServices - } - - def 'concurrency-control with no remember-me or LogoutFilter contains SecurityContextLogoutHandler as LogoutHandlers'() { - when: 'No Logout Filter or RememberMe' - xml.http(['entry-point-ref': 'entryPoint'], { - 'session-management'() { - 'concurrency-control'() - } - }) - bean('entryPoint', 'org.springframework.security.web.authentication.Http403ForbiddenEntryPoint') - createAppContext() - - List filters = getFilters("/someurl") - ConcurrentSessionFilter concurrentSessionFilter = filters.get(1) - def logoutHandlers = concurrentSessionFilter.handlers.logoutHandlers - - then: 'Only SecurityContextLogoutHandler is found in ConcurrentSessionFilter logoutHandlers' - !filters.find { it instanceof LogoutFilter } - logoutHandlers.size() == 1 - def securityCtxlogoutHandler = logoutHandlers.find { it instanceof SecurityContextLogoutHandler } - securityCtxlogoutHandler.invalidateHttpSession == true - } - - def 'SEC-2057: ConcurrentSessionFilter is after SecurityContextPersistenceFilter'() { - httpAutoConfig { - 'session-management'() { - 'concurrency-control'() - } - } - createAppContext() - List filters = getFilters("/someurl") - - expect: - filters.get(0) instanceof SecurityContextPersistenceFilter - filters.get(1) instanceof ConcurrentSessionFilter - } - - def 'concurrency-control handles default expired-url as null'() { - httpAutoConfig { - 'session-management'() { - 'concurrency-control'('session-registry-alias':'sr') - } - } - createAppContext(); - List filters = getFilters("/someurl"); - - expect: - filters.get(1).sessionInformationExpiredStrategy.class.name == 'org.springframework.security.web.session.ConcurrentSessionFilter$ResponseBodySessionInformationExpiredStrategy' - } - - def externalSessionStrategyIsSupported() { - setup: - httpAutoConfig { - 'session-management'('session-authentication-strategy-ref':'ss') - csrf(disabled:true) - } - mockBean(SessionAuthenticationStrategy,'ss') - createAppContext() - - MockHttpServletRequest request = new MockHttpServletRequest(); - request.getSession(); - request.servletPath = "/login" - request.setMethod("POST"); - request.setParameter("username", "user"); - request.setParameter("password", "password"); - - SessionAuthenticationStrategy sessionAuthStrategy = appContext.getBean('ss',SessionAuthenticationStrategy) - FilterChainProxy springSecurityFilterChain = appContext.getBean(FilterChainProxy) - when: - springSecurityFilterChain.doFilter(request,new MockHttpServletResponse(), new MockFilterChain()) - then: "CustomSessionAuthenticationStrategy has seen the request (although REQUEST is a wrapped request)" - verify(sessionAuthStrategy).onAuthentication(any(Authentication), any(HttpServletRequest), any(HttpServletResponse)) - } - - def externalSessionRegistryBeanIsConfiguredCorrectly() { - httpAutoConfig { - 'session-management'() { - 'concurrency-control'('session-registry-ref':'sr') - } - csrf(disabled:true) - } - bean('sr', SessionRegistryImpl.class.name) - createAppContext(); - - expect: - sessionRegistryIsValid(); - } - - def sessionRegistryIsValid() { - Object sessionRegistry = appContext.getBean("sr"); - Object sessionRegistryFromConcurrencyFilter = FieldUtils.getFieldValue( - getFilter(ConcurrentSessionFilter.class), "sessionRegistry"); - Object sessionRegistryFromFormLoginFilter = FieldUtils.getFieldValue(getFilter(UsernamePasswordAuthenticationFilter),"sessionStrategy").delegateStrategies[0].sessionRegistry - Object sessionRegistryFromMgmtFilter = FieldUtils.getFieldValue(getFilter(SessionManagementFilter),"sessionAuthenticationStrategy").delegateStrategies[0].sessionRegistry - - assertSame(sessionRegistry, sessionRegistryFromConcurrencyFilter); - assertSame(sessionRegistry, sessionRegistryFromMgmtFilter); - // SEC-1143 - assertSame(sessionRegistry, sessionRegistryFromFormLoginFilter); - true; - } - - def concurrentSessionMaxSessionsIsCorrectlyConfigured() { - setup: - httpAutoConfig { - 'session-management'('session-authentication-error-url':'/max-exceeded') { - 'concurrency-control'('max-sessions': '2', 'error-if-maximum-exceeded':'true') - } - } - createAppContext(); - - def seshFilter = getFilter(SessionManagementFilter.class); - def auth = new UsernamePasswordAuthenticationToken("bob", "pass"); - SecurityContextHolder.getContext().setAuthentication(auth); - MockHttpServletResponse mockResponse = new MockHttpServletResponse(); - def response = new SaveContextOnUpdateOrErrorResponseWrapper(mockResponse, false) { - protected void saveContext(SecurityContext context) { - } - }; - when: "First session is established" - seshFilter.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()); - then: "ok" - mockResponse.redirectedUrl == null - when: "Second session is established" - seshFilter.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()); - then: "ok" - mockResponse.redirectedUrl == null - when: "Third session is established" - seshFilter.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()); - then: "Rejected" - mockResponse.redirectedUrl == "/max-exceeded"; - } - - def disablingSessionProtectionRemovesSessionManagementFilterIfNoInvalidSessionUrlSet() { - httpAutoConfig { - 'session-management'('session-fixation-protection': 'none') - csrf(disabled:true) - } - createAppContext() - - expect: - !(getFilters("/someurl").find { it instanceof SessionManagementFilter}) - } - - def 'session-fixation-protection=none'() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'POST') - request.session.id = '123' - request.setParameter('username', 'user') - request.setParameter('password', 'password') - request.servletPath = '/login' - - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - httpAutoConfig { - 'session-management'('session-fixation-protection': 'none') - csrf(disabled:true) - } - createAppContext() - request.session.id = '123' - - when: - springSecurityFilterChain.doFilter(request,response, chain) - - then: - request.session.id == '123' - } - - def 'session-fixation-protection=migrateSession'() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'POST') - request.setParameter('username', 'user') - request.setParameter('password', 'password') - request.servletPath = '/login' - - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - httpAutoConfig { - 'session-management'('session-fixation-protection': 'migrateSession') - csrf(disabled:true) - } - createAppContext() - String originalId = request.session.id - - when: - springSecurityFilterChain.doFilter(request,response, chain) - - then: - request.session.id != originalId - } - - def disablingSessionProtectionRetainsSessionManagementFilterInvalidSessionUrlSet() { - httpAutoConfig { - 'session-management'('session-fixation-protection': 'none', 'invalid-session-url': '/timeoutUrl') - csrf(disabled:true) - } - createAppContext() - def filter = getFilters("/someurl")[10] - - expect: - filter instanceof SessionManagementFilter - filter.invalidSessionStrategy.destinationUrl == '/timeoutUrl' - } - -} diff --git a/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java b/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java index ba898c74ae8..d18d95009d2 100644 --- a/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java @@ -108,7 +108,7 @@ public void normalOperationWithNewConfigNonNamespace() throws Exception { public void pathWithNoMatchHasNoFilters() throws Exception { FilterChainProxy filterChainProxy = appCtx.getBean( "newFilterChainProxyNoDefaultPath", FilterChainProxy.class); - assertThat(filterChainProxy.getFilters("/nomatch")).isEqualTo(null); + assertThat(filterChainProxy.getFilters("/nomatch")).isNull(); } // SEC-1235 @@ -152,7 +152,7 @@ private void checkPathAndFilterOrder(FilterChainProxy filterChainProxy) } private void doNormalOperation(FilterChainProxy filterChainProxy) throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.setServletPath("/foo/secure/super/somefile.html"); MockHttpServletResponse response = new MockHttpServletResponse(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java index 295022df38b..2863c9f7758 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java @@ -52,6 +52,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; import java.util.ArrayList; import java.util.Arrays; @@ -60,6 +61,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -395,6 +398,35 @@ PasswordEncoder passwordEncoder() { } } + @Test + public void getAuthenticationWhenUserDetailsServiceAndPasswordManagerThenManagerUsed() throws Exception { + UserDetails user = new User("user", "{noop}password", + AuthorityUtils.createAuthorityList("ROLE_USER")); + this.spring.register(UserDetailsPasswordManagerBeanConfig.class).autowire(); + UserDetailsPasswordManagerBeanConfig.Manager manager = this.spring.getContext().getBean(UserDetailsPasswordManagerBeanConfig.Manager.class); + AuthenticationManager am = this.spring.getContext().getBean(AuthenticationConfiguration.class).getAuthenticationManager(); + when(manager.loadUserByUsername("user")).thenReturn(User.withUserDetails(user).build(), User.withUserDetails(user).build()); + when(manager.updatePassword(any(), any())).thenReturn(user); + + am.authenticate(new UsernamePasswordAuthenticationToken("user", "password")); + + verify(manager).updatePassword(eq(user), startsWith("{bcrypt}")); + } + + @Configuration + @Import({AuthenticationConfiguration.class, ObjectPostProcessorConfiguration.class}) + static class UserDetailsPasswordManagerBeanConfig { + Manager manager = mock(Manager.class); + + @Bean + UserDetailsService userDetailsService() { + return this.manager; + } + + interface Manager extends UserDetailsService, UserDetailsPasswordService { + } + } + //gh-3091 @Test public void getAuthenticationWhenAuthenticationProviderBeanThenUsed() throws Exception { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.java index 99f972f166e..db8c39b5add 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.java @@ -59,7 +59,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.ThrowableAssert.catchThrowable; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -175,6 +177,62 @@ public void onApplicationEvent(AuthenticationSuccessEvent event) { } } + @Test + public void loadConfigWhenInMemoryConfigureProtectedThenPasswordUpgraded() throws Exception { + this.spring.register(InMemoryConfigureProtectedConfig.class).autowire(); + + this.mockMvc.perform(formLogin()) + .andExpect(status().is3xxRedirection()); + + UserDetailsService uds = this.spring.getContext() + .getBean(UserDetailsService.class); + assertThat(uds.loadUserByUsername("user").getPassword()).startsWith("{bcrypt}"); + } + + @EnableWebSecurity + static class InMemoryConfigureProtectedConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + } + + @Override + @Bean + public UserDetailsService userDetailsServiceBean() throws Exception { + return super.userDetailsServiceBean(); + } + } + + @Test + public void loadConfigWhenInMemoryConfigureGlobalThenPasswordUpgraded() throws Exception { + this.spring.register(InMemoryConfigureGlobalConfig.class).autowire(); + + this.mockMvc.perform(formLogin()) + .andExpect(status().is3xxRedirection()); + + UserDetailsService uds = this.spring.getContext() + .getBean(UserDetailsService.class); + assertThat(uds.loadUserByUsername("user").getPassword()).startsWith("{bcrypt}"); + } + + @EnableWebSecurity + static class InMemoryConfigureGlobalConfig extends WebSecurityConfigurerAdapter { + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + } + + @Override + @Bean + public UserDetailsService userDetailsServiceBean() throws Exception { + return super.userDetailsServiceBean(); + } + } + @Test public void loadConfigWhenCustomContentNegotiationStrategyBeanThenOverridesDefault() throws Exception { OverrideContentNegotiationStrategySharedObjectConfig.CONTENT_NEGOTIATION_STRATEGY_BEAN = mock(ContentNegotiationStrategy.class); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java index 3cc4c10105c..4adca37d33a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java @@ -54,7 +54,7 @@ public class WebSecurityTests { @Before public void setup() { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setMethod("GET"); this.response = new MockHttpServletResponse(); this.chain = new MockFilterChain(); 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 c4eb67cd157..eb15b4ef512 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 @@ -17,26 +17,31 @@ import org.junit.Rule; import org.junit.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; -import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import javax.servlet.http.HttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; 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.status; @@ -54,51 +59,31 @@ public class OAuth2ClientConfigurationTests { private MockMvc mockMvc; @Test - public void requestWhenAuthorizedClientFoundThenOAuth2ClientArgumentsResolved() throws Exception { + public void requestWhenAuthorizedClientFoundThenMethodArgumentResolved() throws Exception { String clientRegistrationId = "client1"; String principalName = "user1"; + TestingAuthenticationToken authentication = new TestingAuthenticationToken(principalName, "password"); - ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class); - ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(clientRegistrationId) - .clientId("client-id") - .clientSecret("secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate("{baseUrl}/client1") - .scope("scope1", "scope2") - .authorizationUri("https://provider.com/oauth2/auth") - .tokenUri("https://provider.com/oauth2/token") - .clientName("Client 1") - .build(); - when(clientRegistrationRepository.findByRegistrationId(clientRegistrationId)).thenReturn(clientRegistration); - - OAuth2AuthorizedClientService authorizedClientService = mock(OAuth2AuthorizedClientService.class); + OAuth2AuthorizedClientRepository authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class); OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class); - when(authorizedClientService.loadAuthorizedClient(clientRegistrationId, principalName)).thenReturn(authorizedClient); + when(authorizedClientRepository.loadAuthorizedClient( + eq(clientRegistrationId), eq(authentication), any(HttpServletRequest.class))).thenReturn(authorizedClient); OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); when(authorizedClient.getAccessToken()).thenReturn(accessToken); - OAuth2ClientArgumentResolverConfig.CLIENT_REGISTRATION_REPOSITORY = clientRegistrationRepository; - OAuth2ClientArgumentResolverConfig.AUTHORIZED_CLIENT_SERVICE = authorizedClientService; - this.spring.register(OAuth2ClientArgumentResolverConfig.class).autowire(); + OAuth2AuthorizedClientArgumentResolverConfig.AUTHORIZED_CLIENT_REPOSITORY = authorizedClientRepository; + this.spring.register(OAuth2AuthorizedClientArgumentResolverConfig.class).autowire(); - this.mockMvc.perform(get("/access-token").with(user(principalName))) - .andExpect(status().isOk()) - .andExpect(content().string("resolved")); - this.mockMvc.perform(get("/authorized-client").with(user(principalName))) - .andExpect(status().isOk()) - .andExpect(content().string("resolved")); - this.mockMvc.perform(get("/client-registration").with(user(principalName))) + this.mockMvc.perform(get("/authorized-client").with(authentication(authentication))) .andExpect(status().isOk()) .andExpect(content().string("resolved")); } @EnableWebMvc @EnableWebSecurity - static class OAuth2ClientArgumentResolverConfig extends WebSecurityConfigurerAdapter { - static ClientRegistrationRepository CLIENT_REGISTRATION_REPOSITORY; - static OAuth2AuthorizedClientService AUTHORIZED_CLIENT_SERVICE; + static class OAuth2AuthorizedClientArgumentResolverConfig extends WebSecurityConfigurerAdapter { + static OAuth2AuthorizedClientRepository AUTHORIZED_CLIENT_REPOSITORY; @Override protected void configure(HttpSecurity http) throws Exception { @@ -107,30 +92,121 @@ protected void configure(HttpSecurity http) throws Exception { @RestController public class Controller { - @GetMapping("/access-token") - public String accessToken(@OAuth2Client("client1") OAuth2AccessToken accessToken) { - return accessToken != null ? "resolved" : "not-resolved"; - } - @GetMapping("/authorized-client") - public String authorizedClient(@OAuth2Client("client1") OAuth2AuthorizedClient authorizedClient) { + public String authorizedClient(@RegisteredOAuth2AuthorizedClient("client1") OAuth2AuthorizedClient authorizedClient) { return authorizedClient != null ? "resolved" : "not-resolved"; } + } - @GetMapping("/client-registration") - public String clientRegistration(@OAuth2Client("client1") ClientRegistration clientRegistration) { - return clientRegistration != null ? "resolved" : "not-resolved"; - } + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return mock(ClientRegistrationRepository.class); + } + + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository() { + return AUTHORIZED_CLIENT_REPOSITORY; + } + } + + // gh-5321 + @Test + public void loadContextWhenOAuth2AuthorizedClientRepositoryRegisteredTwiceThenThrowNoUniqueBeanDefinitionException() { + assertThatThrownBy(() -> this.spring.register(OAuth2AuthorizedClientRepositoryRegisteredTwiceConfig.class).autowire()) + .hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class) + .hasMessageContaining("Expected single matching bean of type '" + OAuth2AuthorizedClientRepository.class.getName() + + "' but found 2: authorizedClientRepository1,authorizedClientRepository2"); + } + + @EnableWebMvc + @EnableWebSecurity + static class OAuth2AuthorizedClientRepositoryRegisteredTwiceConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login(); + // @formatter:on } @Bean public ClientRegistrationRepository clientRegistrationRepository() { - return CLIENT_REGISTRATION_REPOSITORY; + return mock(ClientRegistrationRepository.class); + } + + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository1() { + return mock(OAuth2AuthorizedClientRepository.class); + } + + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository2() { + return mock(OAuth2AuthorizedClientRepository.class); + } + } + + @Test + public void loadContextWhenClientRegistrationRepositoryNotRegisteredThenThrowNoSuchBeanDefinitionException() { + assertThatThrownBy(() -> this.spring.register(ClientRegistrationRepositoryNotRegisteredConfig.class).autowire()) + .hasRootCauseInstanceOf(NoSuchBeanDefinitionException.class) + .hasMessageContaining("No qualifying bean of type '" + ClientRegistrationRepository.class.getName() + "' available"); + } + + @EnableWebMvc + @EnableWebSecurity + static class ClientRegistrationRepositoryNotRegisteredConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login(); + // @formatter:on + } + } + + @Test + public void loadContextWhenClientRegistrationRepositoryRegisteredTwiceThenThrowNoUniqueBeanDefinitionException() { + assertThatThrownBy(() -> this.spring.register(ClientRegistrationRepositoryRegisteredTwiceConfig.class).autowire()) + .hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class) + .hasMessageContaining("expected single matching bean but found 2: clientRegistrationRepository1,clientRegistrationRepository2"); + } + + @EnableWebMvc + @EnableWebSecurity + static class ClientRegistrationRepositoryRegisteredTwiceConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login(); + // @formatter:on + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository1() { + return mock(ClientRegistrationRepository.class); + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository2() { + return mock(ClientRegistrationRepository.class); } @Bean - public OAuth2AuthorizedClientService authorizedClientService() { - return AUTHORIZED_CLIENT_SERVICE; + public OAuth2AuthorizedClientRepository authorizedClientRepository() { + return mock(OAuth2AuthorizedClientRepository.class); } } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java index 3ddf21e6d60..51b0a960802 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java @@ -274,7 +274,7 @@ protected void configure(HttpSecurity http) throws Exception { public void securityExpressionHandlerWhenPermissionEvaluatorBeanThenPermissionEvaluatorUsed() throws Exception { this.spring.register(WebSecurityExpressionHandlerPermissionEvaluatorBeanConfig.class).autowire(); TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "notused"); - FilterInvocation invocation = new FilterInvocation(new MockHttpServletRequest(), new MockHttpServletResponse(), new MockFilterChain()); + FilterInvocation invocation = new FilterInvocation(new MockHttpServletRequest("GET", ""), new MockHttpServletResponse(), new MockFilterChain()); AbstractSecurityExpressionHandler handler = this.spring.getContext().getBean(AbstractSecurityExpressionHandler.class); EvaluationContext evaluationContext = handler.createEvaluationContext(authentication, invocation); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java index e05f6e7a015..697ddc29373 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java @@ -68,7 +68,7 @@ public class AuthorizeRequestsTests { @Before public void setup() { this.servletContext = spy(new MockServletContext()); - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setMethod("GET"); this.response = new MockHttpServletResponse(); this.chain = new MockFilterChain(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java new file mode 100644 index 00000000000..75f04401d02 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +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.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Josh Cummings + */ +public class CsrfConfigurerIgnoringRequestMatchersTests { + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void requestWhenIgnoringRequestMatchersThenAugmentedByConfiguredRequestMatcher() + throws Exception { + this.spring.register(IgnoringRequestMatchers.class, BasicController.class).autowire(); + + this.mvc.perform(get("/path")) + .andExpect(status().isForbidden()); + + this.mvc.perform(post("/path")) + .andExpect(status().isOk()); + } + + @EnableWebSecurity + static class IgnoringRequestMatchers extends WebSecurityConfigurerAdapter { + RequestMatcher requestMatcher = + request -> HttpMethod.POST.name().equals(request.getMethod()); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf() + .requireCsrfProtectionMatcher(new AntPathRequestMatcher("/path")) + .ignoringRequestMatchers(this.requestMatcher); + // @formatter:on + } + } + + @Test + public void requestWhenIgnoringRequestMatcherThenUnionsWithConfiguredIgnoringAntMatchers() + throws Exception { + + this.spring.register(IgnoringPathsAndMatchers.class, BasicController.class).autowire(); + + this.mvc.perform(put("/csrf")) + .andExpect(status().isForbidden()); + + this.mvc.perform(post("/csrf")) + .andExpect(status().isOk()); + + this.mvc.perform(put("/no-csrf")) + .andExpect(status().isOk()); + } + + @EnableWebSecurity + static class IgnoringPathsAndMatchers extends WebSecurityConfigurerAdapter { + RequestMatcher requestMatcher = + request -> HttpMethod.POST.name().equals(request.getMethod()); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf() + .ignoringAntMatchers("/no-csrf") + .ignoringRequestMatchers(this.requestMatcher); + // @formatter:on + } + } + + @RestController + public static class BasicController { + @RequestMapping("/path") + public String path() { + return "path"; + } + + @RequestMapping("/csrf") + public String csrf() { + return "csrf"; + } + + @RequestMapping("/no-csrf") + public String noCsrf() { + return "no-csrf"; + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java new file mode 100644 index 00000000000..91c1076d139 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Josh Cummings + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SecurityTestExecutionListeners +public class ExceptionHandlingConfigurerAccessDeniedHandlerTests { + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + @WithMockUser(roles = "ANYTHING") + public void getWhenAccessDeniedOverriddenThenCustomizesResponseByRequest() + throws Exception { + this.spring.register(RequestMatcherBasedAccessDeniedHandlerConfig.class).autowire(); + + this.mvc.perform(get("/hello")) + .andExpect(status().isIAmATeapot()); + + this.mvc.perform(get("/goodbye")) + .andExpect(status().isForbidden()); + } + + @EnableWebSecurity + static class RequestMatcherBasedAccessDeniedHandlerConfig extends WebSecurityConfigurerAdapter { + AccessDeniedHandler teapotDeniedHandler = + (request, response, exception) -> + response.setStatus(HttpStatus.I_AM_A_TEAPOT.value()); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().denyAll() + .and() + .exceptionHandling() + .defaultAccessDeniedHandlerFor( + this.teapotDeniedHandler, + new AntPathRequestMatcher("/hello/**")) + .defaultAccessDeniedHandlerFor( + new AccessDeniedHandlerImpl(), + AnyRequestMatcher.INSTANCE); + // @formatter:on + } + } + + @Test + @WithMockUser(roles = "ANYTHING") + public void getWhenAccessDeniedOverriddenByOnlyOneHandlerThenAllRequestsUseThatHandler() + throws Exception { + this.spring.register(SingleRequestMatcherAccessDeniedHandlerConfig.class).autowire(); + + this.mvc.perform(get("/hello")) + .andExpect(status().isIAmATeapot()); + + this.mvc.perform(get("/goodbye")) + .andExpect(status().isIAmATeapot()); + } + + @EnableWebSecurity + static class SingleRequestMatcherAccessDeniedHandlerConfig extends WebSecurityConfigurerAdapter { + AccessDeniedHandler teapotDeniedHandler = + (request, response, exception) -> + response.setStatus(HttpStatus.I_AM_A_TEAPOT.value()); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().denyAll() + .and() + .exceptionHandling() + .defaultAccessDeniedHandlerFor( + this.teapotDeniedHandler, + new AntPathRequestMatcher("/hello/**")); + // @formatter:on + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java new file mode 100644 index 00000000000..a8e4663988a --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.config.users.AuthenticationTestConfiguration; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class FormLoginConfigurerTests { + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private MockMvc mockMvc; + + @Test + public void requestCache() throws Exception { + this.spring.register(RequestCacheConfig.class, + AuthenticationTestConfiguration.class).autowire(); + + RequestCacheConfig config = this.spring.getContext().getBean(RequestCacheConfig.class); + + this.mockMvc.perform(formLogin()) + .andExpect(authenticated()); + + verify(config.requestCache).getRequest(any(), any()); + } + + @EnableWebSecurity + static class RequestCacheConfig extends WebSecurityConfigurerAdapter { + private RequestCache requestCache = mock(RequestCache.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin().and() + .requestCache() + .requestCache(this.requestCache); + } + } + + @Test + public void requestCacheAsBean() throws Exception { + this.spring.register(RequestCacheBeanConfig.class, + AuthenticationTestConfiguration.class).autowire(); + + RequestCache requestCache = this.spring.getContext().getBean(RequestCache.class); + + this.mockMvc.perform(formLogin()) + .andExpect(authenticated()); + + verify(requestCache).getRequest(any(), any()); + } + + @EnableWebSecurity + static class RequestCacheBeanConfig { + @Bean + RequestCache requestCache() { + return mock(RequestCache.class); + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityAntMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityAntMatchersTests.java index 3a5d338b2e2..fdb6ce5e967 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityAntMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityAntMatchersTests.java @@ -51,7 +51,7 @@ public class HttpSecurityAntMatchersTests { @Before public void setup() { - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); response = new MockHttpServletResponse(); chain = new MockFilterChain(); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityLogoutTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityLogoutTests.java index b327926ab69..30200a83c09 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityLogoutTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityLogoutTests.java @@ -52,7 +52,7 @@ public class HttpSecurityLogoutTests { @Before public void setup() { - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); response = new MockHttpServletResponse(); chain = new MockFilterChain(); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java index 6835425ddf6..1c451a737ff 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java @@ -55,7 +55,7 @@ public class HttpSecurityRequestMatchersTests { @Before public void setup() { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setMethod("GET"); this.response = new MockHttpServletResponse(); this.chain = new MockFilterChain(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java index 441a0175465..6d5113c9bad 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java @@ -72,7 +72,7 @@ public class SessionManagementConfigurerServlet31Tests { @Before public void setup() { - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); response = new MockHttpServletResponse(); chain = new MockFilterChain(); } @@ -88,7 +88,7 @@ public void teardown() { public void changeSessionIdDefaultsInServlet31Plus() throws Exception { spy(ReflectionUtils.class); Method method = mock(Method.class); - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.getSession(); request.setServletPath("/login"); request.setMethod("POST"); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerSessionCreationPolicyTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerSessionCreationPolicyTests.java new file mode 100644 index 00000000000..0d125b89f72 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerSessionCreationPolicyTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Josh Cummings + */ +public class SessionManagementConfigurerSessionCreationPolicyTests { + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void getWhenSharedObjectSessionCreationPolicyConfigurationThenOverrides() + throws Exception { + + this.spring.register(StatelessCreateSessionSharedObjectConfig.class).autowire(); + + MvcResult result = this.mvc.perform(get("/")).andReturn(); + + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @EnableWebSecurity + static class StatelessCreateSessionSharedObjectConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + http.setSharedObject(SessionCreationPolicy.class, SessionCreationPolicy.STATELESS); + } + } + + @Test + public void getWhenUserSessionCreationPolicyConfigurationThenOverrides() + throws Exception { + + this.spring.register(StatelessCreateSessionUserConfig.class).autowire(); + + MvcResult result = this.mvc.perform(get("/")).andReturn(); + + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @EnableWebSecurity + static class StatelessCreateSessionUserConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + http + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + http.setSharedObject(SessionCreationPolicy.class, SessionCreationPolicy.ALWAYS); + } + } + + @Test + public void getWhenDefaultsThenLoginChallengeCreatesSession() + throws Exception { + + this.spring.register(DefaultConfig.class, BasicController.class).autowire(); + + MvcResult result = + this.mvc.perform(get("/")) + .andExpect(status().isUnauthorized()) + .andReturn(); + + assertThat(result.getRequest().getSession(false)).isNotNull(); + } + + @EnableWebSecurity + static class DefaultConfig extends WebSecurityConfigurerAdapter { + } + + @RestController + static class BasicController { + @GetMapping("/") + public String root() { + return "ok"; + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java new file mode 100644 index 00000000000..2430ded2d30 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.Transient; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +/** + * @author Josh Cummings + */ +public class SessionManagementConfigurerTransientAuthenticationTests { + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void postWhenTransientAuthenticationThenNoSessionCreated() + throws Exception { + + this.spring.register(WithTransientAuthenticationConfig.class).autowire(); + MvcResult result = this.mvc.perform(post("/login")).andReturn(); + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @Test + public void postWhenTransientAuthenticationThenAlwaysSessionOverrides() + throws Exception { + + this.spring.register(AlwaysCreateSessionConfig.class).autowire(); + MvcResult result = this.mvc.perform(post("/login")).andReturn(); + assertThat(result.getRequest().getSession(false)).isNotNull(); + } + + @EnableWebSecurity + static class WithTransientAuthenticationConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + + http + .csrf().disable(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .authenticationProvider(new TransientAuthenticationProvider()); + } + } + + @EnableWebSecurity + static class AlwaysCreateSessionConfig extends WithTransientAuthenticationConfig { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS); + } + } + + static class TransientAuthenticationProvider implements AuthenticationProvider { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + return new SomeTransientAuthentication(); + } + + @Override + public boolean supports(Class authentication) { + return true; + } + } + + @Transient + static class SomeTransientAuthentication extends AbstractAuthenticationToken { + SomeTransientAuthentication() { + super(null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java index 0f47ac85602..2a598af47af 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java @@ -55,7 +55,7 @@ public class UrlAuthorizationConfigurerTests { @Before public void setup() { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setMethod("GET"); this.response = new MockHttpServletResponse(); this.chain = new MockFilterChain(); @@ -211,4 +211,4 @@ public void loadConfig(Class... configs) { this.context.getAutowireCapableBeanFactory().autowireBean(this); } -} \ No newline at end of file +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java index 08917c1bad2..795b5bc2339 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java @@ -23,6 +23,7 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -30,14 +31,18 @@ import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; 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.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; @@ -59,6 +64,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; @@ -74,6 +80,10 @@ public class OAuth2ClientConfigurerTests { private static OAuth2AuthorizedClientService authorizedClientService; + private static OAuth2AuthorizedClientRepository authorizedClientRepository; + + private static OAuth2AuthorizationRequestResolver authorizationRequestResolver; + private static OAuth2AccessTokenResponseClient accessTokenResponseClient; private static RequestCache requestCache; @@ -103,6 +113,9 @@ public void setup() { .build(); clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1); authorizedClientService = new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); + authorizedClientRepository = new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); + authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( + clientRegistrationRepository, "/oauth2/authorization"); OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("access-token-1234") .tokenType(OAuth2AccessToken.TokenType.BEARER) @@ -120,7 +133,7 @@ public void configureWhenAuthorizationCodeRequestThenRedirectForAuthorization() MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/authorization/registration-1")) .andExpect(status().is3xxRedirection()) .andReturn(); - assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/client-1"); + assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fclient-1"); } @Test @@ -140,24 +153,25 @@ public void configureWhenAuthorizationCodeResponseSuccessThenAuthorizedClientSav AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); MockHttpServletResponse response = new MockHttpServletResponse(); authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); MockHttpSession session = (MockHttpSession) request.getSession(); String principalName = "user1"; + TestingAuthenticationToken authentication = new TestingAuthenticationToken(principalName, "password"); this.mockMvc.perform(get("/client-1") .param(OAuth2ParameterNames.CODE, "code") .param(OAuth2ParameterNames.STATE, "state") - .with(user(principalName)) + .with(authentication(authentication)) .session(session)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("http://localhost/client-1")); - OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient( - this.registration1.getRegistrationId(), principalName); + OAuth2AuthorizedClient authorizedClient = authorizedClientRepository.loadAuthorizedClient( + this.registration1.getRegistrationId(), authentication, request); assertThat(authorizedClient).isNotNull(); } @@ -168,11 +182,33 @@ public void configureWhenRequestCacheProvidedAndClientAuthorizationRequiredExcep MvcResult mvcResult = this.mockMvc.perform(get("/resource1").with(user("user1"))) .andExpect(status().is3xxRedirection()) .andReturn(); - assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/client-1"); + assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fclient-1"); verify(requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class)); } + // gh-5521 + @Test + public void configureWhenCustomAuthorizationRequestResolverSetThenAuthorizationRequestIncludesCustomParameters() throws Exception { + // Override default resolver + OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver = authorizationRequestResolver; + authorizationRequestResolver = request -> { + OAuth2AuthorizationRequest defaultAuthorizationRequest = defaultAuthorizationRequestResolver.resolve(request); + Map additionalParameters = new HashMap<>(defaultAuthorizationRequest.getAdditionalParameters()); + additionalParameters.put("param1", "value1"); + return OAuth2AuthorizationRequest.from(defaultAuthorizationRequest) + .additionalParameters(additionalParameters) + .build(); + }; + + this.spring.register(OAuth2ClientConfig.class).autowire(); + + MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/authorization/registration-1")) + .andExpect(status().is3xxRedirection()) + .andReturn(); + assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fclient-1¶m1=value1"); + } + @EnableWebSecurity @EnableWebMvc static class OAuth2ClientConfig extends WebSecurityConfigurerAdapter { @@ -188,6 +224,9 @@ protected void configure(HttpSecurity http) throws Exception { .oauth2() .client() .authorizationCodeGrant() + .authorizationEndpoint() + .authorizationRequestResolver(authorizationRequestResolver) + .and() .tokenEndpoint() .accessTokenResponseClient(accessTokenResponseClient); } @@ -198,14 +237,14 @@ public ClientRegistrationRepository clientRegistrationRepository() { } @Bean - public OAuth2AuthorizedClientService authorizedClientService() { - return authorizedClientService; + public OAuth2AuthorizedClientRepository authorizedClientRepository() { + return authorizedClientRepository; } @RestController public class ResourceController { @GetMapping("/resource1") - public String resource1(@OAuth2Client("registration-1") OAuth2AuthorizedClient authorizedClient) { + public String resource1(@RegisteredOAuth2AuthorizedClient("registration-1") OAuth2AuthorizedClient authorizedClient) { return "resource1"; } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index 203143fec13..01a7111cfcb 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -15,6 +15,7 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.client; +import org.apache.http.HttpHeaders; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -22,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -42,7 +44,9 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; @@ -79,14 +83,19 @@ * Tests for {@link OAuth2LoginConfigurer}. * * @author Kazuki Shimizu + * @author Joe Grandja * @since 5.0.1 */ public class OAuth2LoginConfigurerTests { - private static final ClientRegistration CLIENT_REGISTRATION = CommonOAuth2Provider.GOOGLE + private static final ClientRegistration GOOGLE_CLIENT_REGISTRATION = CommonOAuth2Provider.GOOGLE .getBuilder("google").clientId("clientId").clientSecret("clientSecret") .build(); + private static final ClientRegistration GITHUB_CLIENT_REGISTRATION = CommonOAuth2Provider.GITHUB + .getBuilder("github").clientId("clientId").clientSecret("clientSecret") + .build(); + private ConfigurableApplicationContext context; @Autowired @@ -104,12 +113,10 @@ public class OAuth2LoginConfigurerTests { @Before public void setup() { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); + this.request.setServletPath("/login/oauth2/code/google"); this.response = new MockHttpServletResponse(); this.filterChain = new MockFilterChain(); - - this.request.setMethod("GET"); - this.request.setServletPath("/login/oauth2/code/google"); } @After @@ -197,6 +204,104 @@ public void oauth2LoginCustomWithBeanRegistration() throws Exception { assertThat(authentication.getAuthorities()).last().hasToString("ROLE_OAUTH2_USER"); } + // gh-5488 + @Test + public void oauth2LoginConfigLoginProcessingUrl() throws Exception { + // setup application context + loadConfig(OAuth2LoginConfigLoginProcessingUrl.class); + + // setup authorization request + OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest(); + this.request.setServletPath("/login/oauth2/google"); + this.authorizationRequestRepository.saveAuthorizationRequest( + authorizationRequest, this.request, this.response); + + // setup authentication parameters + this.request.setParameter("code", "code123"); + this.request.setParameter("state", authorizationRequest.getState()); + + // perform test + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + // assertions + Authentication authentication = this.securityContextRepository + .loadContext(new HttpRequestResponseHolder(this.request, this.response)) + .getAuthentication(); + assertThat(authentication.getAuthorities()).hasSize(1); + assertThat(authentication.getAuthorities()).first() + .isInstanceOf(OAuth2UserAuthority.class).hasToString("ROLE_USER"); + } + + // gh-5521 + @Test + public void oauth2LoginWithCustomAuthorizationRequestParameters() throws Exception { + loadConfig(OAuth2LoginConfigCustomAuthorizationRequestResolver.class); + + String requestUri = "/oauth2/authorization/google"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).matches("https://accounts.google.com/o/oauth2/v2/auth\\?response_type=code&client_id=clientId&scope=openid\\+profile\\+email&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fgoogle&custom-param1=custom-value1"); + } + + // gh-5347 + @Test + public void oauth2LoginWithOneClientConfiguredThenRedirectForAuthorization() throws Exception { + loadConfig(OAuth2LoginConfig.class); + + String requestUri = "/"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/oauth2/authorization/google"); + } + + // gh-5347 + @Test + public void oauth2LoginWithOneClientConfiguredAndRequestFaviconNotAuthenticatedThenRedirectDefaultLoginPage() throws Exception { + loadConfig(OAuth2LoginConfig.class); + + String requestUri = "/favicon.ico"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + this.request.addHeader(HttpHeaders.ACCEPT, new MediaType("image", "*").toString()); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/login"); + } + + // gh-5347 + @Test + public void oauth2LoginWithMultipleClientsConfiguredThenRedirectDefaultLoginPage() throws Exception { + loadConfig(OAuth2LoginConfigMultipleClients.class); + + String requestUri = "/"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/login"); + } + + @Test + public void oauth2LoginWithCustomLoginPageThenRedirectCustomLoginPage() throws Exception { + loadConfig(OAuth2LoginConfigCustomLoginPage.class); + + String requestUri = "/"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/custom-login"); + } + @Test public void oidcLogin() throws Exception { // setup application context @@ -306,15 +411,19 @@ private void registerJwtDecoder() { } private OAuth2AuthorizationRequest createOAuth2AuthorizationRequest(String... scopes) { + return this.createOAuth2AuthorizationRequest(GOOGLE_CLIENT_REGISTRATION, scopes); + } + + private OAuth2AuthorizationRequest createOAuth2AuthorizationRequest(ClientRegistration registration, String... scopes) { return OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(CLIENT_REGISTRATION.getProviderDetails().getAuthorizationUri()) - .clientId(CLIENT_REGISTRATION.getClientId()) + .authorizationUri(registration.getProviderDetails().getAuthorizationUri()) + .clientId(registration.getClientId()) .state("state123") .redirectUri("http://localhost") .additionalParameters( - Collections.singletonMap( - OAuth2ParameterNames.REGISTRATION_ID, - CLIENT_REGISTRATION.getRegistrationId())) + Collections.singletonMap( + OAuth2ParameterNames.REGISTRATION_ID, + registration.getRegistrationId())) .scope(scopes) .build(); } @@ -326,7 +435,7 @@ protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .clientRegistrationRepository( - new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION)); + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)); super.configure(http); } } @@ -338,7 +447,7 @@ protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .clientRegistrationRepository( - new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION)) + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) .userInfoEndpoint() .userAuthoritiesMapper(createGrantedAuthoritiesMapper()); super.configure(http); @@ -356,7 +465,7 @@ protected void configure(HttpSecurity http) throws Exception { @Bean ClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryClientRegistrationRepository(CLIENT_REGISTRATION); + return new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION); } @Bean @@ -365,10 +474,81 @@ GrantedAuthoritiesMapper grantedAuthoritiesMapper() { } } + @EnableWebSecurity + static class OAuth2LoginConfigLoginProcessingUrl extends CommonWebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .oauth2Login() + .clientRegistrationRepository( + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) + .loginProcessingUrl("/login/oauth2/*"); + super.configure(http); + } + } + + @EnableWebSecurity + static class OAuth2LoginConfigCustomAuthorizationRequestResolver extends CommonWebSecurityConfigurerAdapter { + private ClientRegistrationRepository clientRegistrationRepository = + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION); + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .oauth2Login() + .clientRegistrationRepository(this.clientRegistrationRepository) + .authorizationEndpoint() + .authorizationRequestResolver(this.getAuthorizationRequestResolver()); + super.configure(http); + } + + private OAuth2AuthorizationRequestResolver getAuthorizationRequestResolver() { + OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver = + new DefaultOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository, "/oauth2/authorization"); + return request -> { + OAuth2AuthorizationRequest defaultAuthorizationRequest = defaultAuthorizationRequestResolver.resolve(request); + Map additionalParameters = new HashMap<>(defaultAuthorizationRequest.getAdditionalParameters()); + additionalParameters.put("custom-param1", "custom-value1"); + return OAuth2AuthorizationRequest.from(defaultAuthorizationRequest) + .additionalParameters(additionalParameters) + .build(); + }; + } + } + + @EnableWebSecurity + static class OAuth2LoginConfigMultipleClients extends CommonWebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .oauth2Login() + .clientRegistrationRepository( + new InMemoryClientRegistrationRepository( + GOOGLE_CLIENT_REGISTRATION, GITHUB_CLIENT_REGISTRATION)); + super.configure(http); + } + } + + @EnableWebSecurity + static class OAuth2LoginConfigCustomLoginPage extends CommonWebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .oauth2Login() + .clientRegistrationRepository( + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) + .loginPage("/custom-login"); + super.configure(http); + } + } + private static abstract class CommonWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http + .authorizeRequests() + .anyRequest().authenticated() + .and() .securityContext() .securityContextRepository(securityContextRepository()) .and() 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 new file mode 100644 index 00000000000..d592f0664ca --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -0,0 +1,1558 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers.oauth2.server.resource; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Field; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.PreDestroy; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.hamcrest.core.AllOf; +import org.hamcrest.core.StringContains; +import org.hamcrest.core.StringEndsWith; +import org.hamcrest.core.StringStartsWith; +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport; +import org.springframework.security.oauth2.jwt.JwtTimestampValidator; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +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.context.support.GenericWebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.core.StringStartsWith.startsWith; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +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.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +/** + * Tests for {@link OAuth2ResourceServerConfigurer} + * + * @author Josh Cummings + */ +public class OAuth2ResourceServerConfigurerTests { + private static final String JWT_TOKEN = "token"; + private static final String JWT_SUBJECT = "mock-test-subject"; + private static final Map JWT_HEADERS = Collections.singletonMap("alg", JwsAlgorithms.RS256); + private static final Map JWT_CLAIMS = Collections.singletonMap(JwtClaimNames.SUB, JWT_SUBJECT); + private static final Jwt JWT = new Jwt(JWT_TOKEN, Instant.MIN, Instant.MAX, JWT_HEADERS, JWT_CLAIMS); + private static final String JWK_SET_URI = "https://mock.org"; + + @Autowired(required = false) + MockMvc mvc; + + @Autowired(required = false) + MockWebServer authz; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void getWhenUsingDefaultsWithValidBearerTokenThenAcceptsRequest() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("ok")); + } + + @Test + public void getWhenUsingDefaultsWithExpiredBearerTokenThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("Expired"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt")); + } + + @Test + public void getWhenUsingDefaultsWithBadJwkEndpointThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire(); + this.authz.enqueue(new MockResponse().setBody("malformed")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Malformed Jwk set")); + } + + @Test + public void getWhenUsingDefaultsWithUnavailableJwkEndpointThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire(); + this.authz.shutdown(); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt")); + } + + @Test + public void getWhenUsingDefaultsWithMalformedBearerTokenThenInvalidToken() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + + this.mvc.perform(get("/").with(bearerToken("an\"invalid\"token"))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("Bearer token is malformed")); + } + + @Test + public void getWhenUsingDefaultsWithMalformedPayloadThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("MalformedPayload"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Malformed payload")); + } + + @Test + public void getWhenUsingDefaultsWithUnsignedBearerTokenThenInvalidToken() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + String token = this.token("Unsigned"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("Unsupported algorithm of none")); + } + + @Test + public void getWhenUsingDefaultsWithBearerTokenBeforeNotBeforeThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("TooEarly"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt")); + } + + @Test + public void getWhenUsingDefaultsWithBearerTokenInTwoPlacesThenInvalidRequest() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + + this.mvc.perform(get("/") + .with(bearerToken("token")) + .with(bearerToken("token").asParam())) + .andExpect(status().isBadRequest()) + .andExpect(invalidRequestHeader("Found multiple bearer tokens in the request")); + } + + @Test + public void getWhenUsingDefaultsWithBearerTokenInTwoParametersThenInvalidRequest() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("access_token", "token1"); + params.add("access_token", "token2"); + + this.mvc.perform(get("/") + .params(params)) + .andExpect(status().isBadRequest()) + .andExpect(invalidRequestHeader("Found multiple bearer tokens in the request")); + } + + @Test + public void postWhenUsingDefaultsWithBearerTokenAsFormParameterThenIgnoresToken() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + + this.mvc.perform(post("/") // engage csrf + .with(bearerToken("token").asParam())) + .andExpect(status().isForbidden()) + .andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE)); + } + + @Test + public void postWhenCsrfDisabledWithBearerTokenAsFormParameterThenIgnoresToken() + throws Exception { + + this.spring.register(CsrfDisabledConfig.class).autowire(); + + this.mvc.perform(post("/") + .with(bearerToken("token").asParam())) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer")); + } + + @Test + public void getWhenUsingDefaultsWithNoBearerTokenThenUnauthorized() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer")); + } + + @Test + public void getWhenUsingDefaultsWithSufficientlyScopedBearerTokenThenAcceptsRequest() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidMessageReadScope"); + + this.mvc.perform(get("/requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("SCOPE_message:read")); + } + + @Test + public void getWhenUsingDefaultsWithInsufficientScopeThenInsufficientScopeError() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isForbidden()) + .andExpect(insufficientScopeHeader("")); + } + + @Test + public void getWhenUsingDefaultsWithInsufficientScpThenInsufficientScopeError() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidMessageWriteScp"); + + this.mvc.perform(get("/requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isForbidden()) + .andExpect(insufficientScopeHeader("message:write")); + } + + @Test + public void getWhenUsingDefaultsAndAuthorizationServerHasNoMatchingKeyThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire(); + this.authz.enqueue(this.jwks("Empty")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/") + .with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt")); + } + + @Test + public void getWhenUsingDefaultsAndAuthorizationServerHasMultipleMatchingKeysThenOk() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("TwoKeys")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("test-subject")); + } + + @Test + public void getWhenUsingDefaultsAndKeyMatchesByKidThenOk() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("TwoKeys")); + String token = this.token("Kid"); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("test-subject")); + } + + // -- Method Security + + @Test + public void getWhenUsingMethodSecurityWithValidBearerTokenThenAcceptsRequest() + throws Exception { + + this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidMessageReadScope"); + + this.mvc.perform(get("/ms-requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("SCOPE_message:read")); + } + + @Test + public void getWhenUsingMethodSecurityWithValidBearerTokenHavingScpAttributeThenAcceptsRequest() + throws Exception { + + this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidMessageReadScp"); + + this.mvc.perform(get("/ms-requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("SCOPE_message:read")); + } + + @Test + public void getWhenUsingMethodSecurityWithInsufficientScopeThenInsufficientScopeError() + throws Exception { + + this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/ms-requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isForbidden()) + .andExpect(insufficientScopeHeader("")); + + } + + @Test + public void getWhenUsingMethodSecurityWithInsufficientScpThenInsufficientScopeError() + throws Exception { + + this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidMessageWriteScp"); + + this.mvc.perform(get("/ms-requires-read-scope") + .with(bearerToken(token))) + .andExpect(status().isForbidden()) + .andExpect(insufficientScopeHeader("message:write")); + } + + @Test + public void getWhenUsingMethodSecurityWithDenyAllThenInsufficientScopeError() + throws Exception { + + this.spring.register(WebServerConfig.class, MethodSecurityConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidMessageReadScope"); + + this.mvc.perform(get("/ms-deny") + .with(bearerToken(token))) + .andExpect(status().isForbidden()) + .andExpect(insufficientScopeHeader("message:read")); + } + + // -- Resource Server should not engage csrf + + @Test + public void postWhenUsingDefaultsWithValidBearerTokenAndNoCsrfTokenThenOk() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(post("/authenticated") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("test-subject")); + } + + @Test + public void postWhenUsingDefaultsWithNoBearerTokenThenCsrfDenies() + throws Exception { + + this.spring.register(DefaultConfig.class).autowire(); + + this.mvc.perform(post("/authenticated")) + .andExpect(status().isForbidden()) + .andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE)); + } + + @Test + public void postWhenUsingDefaultsWithExpiredBearerTokenAndNoCsrfThenInvalidToken() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("Expired"); + + this.mvc.perform(post("/authenticated") + .with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt")); + } + + // -- Resource Server should not create sessions + + @Test + public void requestWhenDefaultConfiguredThenSessionIsNotCreated() + throws Exception { + + this.spring.register(WebServerConfig.class, DefaultConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + MvcResult result = this.mvc.perform(get("/") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andReturn(); + + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @Test + public void requestWhenUsingDefaultsAndNoBearerTokenThenSessionIsNotCreated() + throws Exception { + + this.spring.register(DefaultConfig.class, BasicController.class).autowire(); + + MvcResult result = this.mvc.perform(get("/")) + .andExpect(status().isUnauthorized()) + .andReturn(); + + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @Test + public void requestWhenSessionManagementConfiguredThenUserConfigurationOverrides() + throws Exception { + + this.spring.register(WebServerConfig.class, AlwaysSessionCreationConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + MvcResult result = this.mvc.perform(get("/") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andReturn(); + + assertThat(result.getRequest().getSession(false)).isNotNull(); + } + + // -- custom bearer token resolver + + @Test + public void requestWhenBearerTokenResolverAllowsRequestBodyThenEitherHeaderOrRequestBodyIsAccepted() + throws Exception { + + this.spring.register(AllowBearerTokenInRequestBodyConfig.class, JwtDecoderConfig.class, + BasicController.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN))) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + + this.mvc.perform(post("/authenticated") + .param("access_token", JWT_TOKEN)) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + } + + @Test + public void requestWhenBearerTokenResolverAllowsQueryParameterThenEitherHeaderOrQueryParameterIsAccepted() + throws Exception { + + this.spring.register(AllowBearerTokenAsQueryParameterConfig.class, JwtDecoderConfig.class, + BasicController.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN))) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + + this.mvc.perform(get("/authenticated") + .param("access_token", JWT_TOKEN)) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + } + + @Test + public void requestWhenBearerTokenResolverAllowsRequestBodyAndRequestContainsTwoTokensThenInvalidRequest() + throws Exception { + + this.spring.register(AllowBearerTokenInRequestBodyConfig.class, JwtDecoderConfig.class, + BasicController.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(post("/authenticated") + .param("access_token", JWT_TOKEN) + .with(bearerToken(JWT_TOKEN)) + .with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("invalid_request"))); + } + + @Test + public void requestWhenBearerTokenResolverAllowsQueryParameterAndRequestContainsTwoTokensThenInvalidRequest() + throws Exception { + + this.spring.register(AllowBearerTokenAsQueryParameterConfig.class, JwtDecoderConfig.class, + BasicController.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN)) + .param("access_token", JWT_TOKEN)) + .andExpect(status().isBadRequest()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("invalid_request"))); + } + + @Test + public void getBearerTokenResolverWhenDuplicateResolverBeansAndAnotherOnTheDslThenTheDslOneIsUsed() { + BearerTokenResolver resolverBean = mock(BearerTokenResolver.class); + BearerTokenResolver resolver = mock(BearerTokenResolver.class); + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("resolverOne", BearerTokenResolver.class, () -> resolverBean); + context.registerBean("resolverTwo", BearerTokenResolver.class, () -> resolverBean); + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context); + + oauth2.bearerTokenResolver(resolver); + + assertThat(oauth2.getBearerTokenResolver()).isEqualTo(resolver); + } + + @Test + public void getBearerTokenResolverWhenDuplicateResolverBeansThenWiringException() { + assertThatCode(() -> this.spring.register(MultipleBearerTokenResolverBeansConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class); + } + + @Test + public void getBearerTokenResolverWhenResolverBeanAndAnotherOnTheDslThenTheDslOneIsUsed() { + BearerTokenResolver resolver = mock(BearerTokenResolver.class); + BearerTokenResolver resolverBean = mock(BearerTokenResolver.class); + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean(BearerTokenResolver.class, () -> resolverBean); + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context); + oauth2.bearerTokenResolver(resolver); + + assertThat(oauth2.getBearerTokenResolver()).isEqualTo(resolver); + } + + @Test + public void getBearerTokenResolverWhenNoResolverSpecifiedThenTheDefaultIsUsed() { + ApplicationContext context = + this.spring.context(new GenericWebApplicationContext()).getContext(); + + OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context); + + assertThat(oauth2.getBearerTokenResolver()).isInstanceOf(DefaultBearerTokenResolver.class); + } + + // -- custom jwt decoder + + @Test + public void requestWhenCustomJwtDecoderWiredOnDslThenUsed() + throws Exception { + + this.spring.register(CustomJwtDecoderOnDsl.class, BasicController.class).autowire(); + + CustomJwtDecoderOnDsl config = this.spring.getContext().getBean(CustomJwtDecoderOnDsl.class); + JwtDecoder decoder = config.decoder(); + + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN))) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + } + + @Test + public void requestWhenCustomJwtDecoderExposedAsBeanThenUsed() + throws Exception { + + this.spring.register(CustomJwtDecoderAsBean.class, BasicController.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN))) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + } + + @Test + public void getJwtDecoderWhenConfiguredWithDecoderAndJwkSetUriThenLastOneWins() { + ApplicationContext context = mock(ApplicationContext.class); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer(context).jwt(); + + JwtDecoder decoder = mock(JwtDecoder.class); + + jwtConfigurer.jwkSetUri(JWK_SET_URI); + jwtConfigurer.decoder(decoder); + + assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder); + + jwtConfigurer = + new OAuth2ResourceServerConfigurer(context).jwt(); + + jwtConfigurer.decoder(decoder); + jwtConfigurer.jwkSetUri(JWK_SET_URI); + + assertThat(jwtConfigurer.getJwtDecoder()).isInstanceOf(NimbusJwtDecoderJwkSupport.class); + + } + + @Test + public void getJwtDecoderWhenConflictingJwtDecodersThenTheDslWiredOneTakesPrecedence() { + + JwtDecoder decoderBean = mock(JwtDecoder.class); + JwtDecoder decoder = mock(JwtDecoder.class); + + ApplicationContext context = mock(ApplicationContext.class); + when(context.getBean(JwtDecoder.class)).thenReturn(decoderBean); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer(context).jwt(); + jwtConfigurer.decoder(decoder); + + assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder); + } + + @Test + public void getJwtDecoderWhenContextHasBeanAndUserConfiguresJwkSetUriThenJwkSetUriTakesPrecedence() { + + JwtDecoder decoder = mock(JwtDecoder.class); + ApplicationContext context = mock(ApplicationContext.class); + when(context.getBean(JwtDecoder.class)).thenReturn(decoder); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer(context).jwt(); + + jwtConfigurer.jwkSetUri(JWK_SET_URI); + + assertThat(jwtConfigurer.getJwtDecoder()).isNotEqualTo(decoder); + assertThat(jwtConfigurer.getJwtDecoder()).isInstanceOf(NimbusJwtDecoderJwkSupport.class); + } + + @Test + public void getJwtDecoderWhenTwoJwtDecoderBeansAndAnotherWiredOnDslThenDslWiredOneTakesPrecedence() { + + JwtDecoder decoderBean = mock(JwtDecoder.class); + JwtDecoder decoder = mock(JwtDecoder.class); + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("decoderOne", JwtDecoder.class, () -> decoderBean); + context.registerBean("decoderTwo", JwtDecoder.class, () -> decoderBean); + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer(context).jwt(); + jwtConfigurer.decoder(decoder); + + assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder); + } + + @Test + public void getJwtDecoderWhenTwoJwtDecoderBeansThenThrowsException() { + + JwtDecoder decoder = mock(JwtDecoder.class); + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("decoderOne", JwtDecoder.class, () -> decoder); + context.registerBean("decoderTwo", JwtDecoder.class, () -> decoder); + + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer(context).jwt(); + + assertThatCode(() -> jwtConfigurer.getJwtDecoder()) + .isInstanceOf(NoUniqueBeanDefinitionException.class); + } + + // -- exception handling + + @Test + public void requestWhenRealmNameConfiguredThenUsesOnUnauthenticated() + throws Exception { + + this.spring.register(RealmNameConfiguredOnEntryPoint.class, JwtDecoderConfig.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenThrow(JwtException.class); + + this.mvc.perform(get("/authenticated") + .with(bearerToken("invalid_token"))) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer realm=\"myRealm\""))); + } + + @Test + public void requestWhenRealmNameConfiguredThenUsesOnAccessDenied() + throws Exception { + + this.spring.register(RealmNameConfiguredOnAccessDeniedHandler.class, JwtDecoderConfig.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken("insufficiently_scoped"))) + .andExpect(status().isForbidden()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer realm=\"myRealm\""))); + } + + @Test + public void authenticationEntryPointWhenGivenNullThenThrowsException() { + ApplicationContext context = mock(ApplicationContext.class); + OAuth2ResourceServerConfigurer configurer = new OAuth2ResourceServerConfigurer(context); + assertThatCode(() -> configurer.authenticationEntryPoint(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void accessDeniedHandlerWhenGivenNullThenThrowsException() { + ApplicationContext context = mock(ApplicationContext.class); + OAuth2ResourceServerConfigurer configurer = new OAuth2ResourceServerConfigurer(context); + assertThatCode(() -> configurer.accessDeniedHandler(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + // -- token validator + + @Test + public void requestWhenCustomJwtValidatorFailsThenCorrespondingErrorMessage() + throws Exception { + + this.spring.register(WebServerConfig.class, CustomJwtValidatorConfig.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + OAuth2TokenValidator jwtValidator = + this.spring.getContext().getBean(CustomJwtValidatorConfig.class) + .getJwtValidator(); + + OAuth2Error error = new OAuth2Error("custom-error", "custom-description", "custom-uri"); + + when(jwtValidator.validate(any(Jwt.class))).thenReturn(OAuth2TokenValidatorResult.failure(error)); + + this.mvc.perform(get("/") + .with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("custom-description"))); + } + + @Test + public void requestWhenClockSkewSetThenTimestampWindowRelaxedAccordingly() + throws Exception { + + this.spring.register(WebServerConfig.class, UnexpiredJwtClockSkewConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ExpiresAt4687177990"); + + this.mvc.perform(get("/") + .with(bearerToken(token))) + .andExpect(status().isOk()); + } + + @Test + public void requestWhenClockSkewSetButJwtStillTooLateThenReportsExpired() + throws Exception { + + this.spring.register(WebServerConfig.class, ExpiredJwtClockSkewConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ExpiresAt4687177990"); + + this.mvc.perform(get("/") + .with(bearerToken(token))) + .andExpect(status().isUnauthorized()) + .andExpect(invalidTokenHeader("Jwt expired at")); + } + + // -- In combination with other authentication providers + + @Test + public void requestWhenBasicAndResourceServerEntryPointsThenMatchedByRequest() + throws Exception { + + this.spring.register(BasicAndResourceServerConfig.class, JwtDecoderConfig.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenThrow(JwtException.class); + + this.mvc.perform(get("/authenticated") + .with(httpBasic("some", "user"))) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Basic"))); + + this.mvc.perform(get("/authenticated")) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Basic"))); + + this.mvc.perform(get("/authenticated") + .with(bearerToken("invalid_token"))) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer"))); + } + + @Test + public void requestWhenDefaultAndResourceServerAccessDeniedHandlersThenMatchedByRequest() + throws Exception { + + this.spring.register(ExceptionHandlingAndResourceServerWithAccessDeniedHandlerConfig.class, + JwtDecoderConfig.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(httpBasic("basic-user", "basic-password"))) + .andExpect(status().isForbidden()) + .andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE)); + + this.mvc.perform(get("/authenticated") + .with(bearerToken("insufficiently_scoped"))) + .andExpect(status().isForbidden()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer"))); + } + + @Test + public void getWhenAlsoUsingHttpBasicThenCorrectProviderEngages() + throws Exception { + + this.spring.register(WebServerConfig.class, BasicAndResourceServerConfig.class, BasicController.class).autowire(); + this.authz.enqueue(this.jwks("Default")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("test-subject")); + + this.mvc.perform(get("/authenticated") + .with(httpBasic("basic-user", "basic-password"))) + .andExpect(status().isOk()) + .andExpect(content().string("basic-user")); + } + + // -- Incorrect Configuration + + @Test + public void configuredWhenMissingJwtAuthenticationProviderThenWiringException() { + + assertThatCode(() -> this.spring.register(JwtlessConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasMessageContaining("no Jwt configuration was found"); + } + + @Test + public void configureWhenMissingJwkSetUriThenWiringException() { + + assertThatCode(() -> this.spring.register(JwtHalfConfiguredConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasMessageContaining("No qualifying bean of type"); + } + + // -- support + + @EnableWebSecurity + static class DefaultConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri:https://example.org}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .antMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')") + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(this.uri); + // @formatter:on + } + } + + @EnableWebSecurity + static class CsrfDisabledConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri:https://example.org}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .antMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')") + .anyRequest().authenticated() + .and() + .csrf().disable() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(this.uri); + // @formatter:on + } + } + + @EnableWebSecurity + @EnableGlobalMethodSecurity(prePostEnabled = true) + static class MethodSecurityConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri:https://example.org}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(this.uri); + // @formatter:on + } + } + + @EnableWebSecurity + static class JwtlessConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer(); + // @formatter:on + } + } + + @EnableWebSecurity + static class RealmNameConfiguredOnEntryPoint extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .authenticationEntryPoint(authenticationEntryPoint()) + .jwt(); + // @formatter:on + } + + AuthenticationEntryPoint authenticationEntryPoint() { + BearerTokenAuthenticationEntryPoint entryPoint = + new BearerTokenAuthenticationEntryPoint(); + entryPoint.setRealmName("myRealm"); + return entryPoint; + } + } + + @EnableWebSecurity + static class RealmNameConfiguredOnAccessDeniedHandler extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().denyAll() + .and() + .oauth2() + .resourceServer() + .accessDeniedHandler(accessDeniedHandler()) + .jwt(); + // @formatter:on + } + + AccessDeniedHandler accessDeniedHandler() { + BearerTokenAccessDeniedHandler accessDeniedHandler = + new BearerTokenAccessDeniedHandler(); + accessDeniedHandler.setRealmName("myRealm"); + return accessDeniedHandler; + } + } + + @EnableWebSecurity + static class ExceptionHandlingAndResourceServerWithAccessDeniedHandlerConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().denyAll() + .and() + .exceptionHandling() + .defaultAccessDeniedHandlerFor(new AccessDeniedHandlerImpl(), request -> false) + .and() + .httpBasic() + .and() + .oauth2() + .resourceServer() + .jwt(); + // @formatter:on + } + + @Bean + public UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager( + org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder() + .username("basic-user") + .password("basic-password") + .roles("USER") + .build()); + } + } + + @EnableWebSecurity + static class BasicAndResourceServerConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri:https://example.org}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .httpBasic() + .and() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(this.uri); + // @formatter:on + } + + @Bean + public UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager( + org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder() + .username("basic-user") + .password("basic-password") + .roles("USER") + .build()); + } + } + + @EnableWebSecurity + static class JwtHalfConfiguredConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt(); // missing key configuration, e.g. jwkSetUri + // @formatter:on + } + } + + @EnableWebSecurity + static class AlwaysSessionCreationConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.ALWAYS) + .and() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(this.uri); + // @formatter:on + } + } + + @EnableWebSecurity + static class AllowBearerTokenInRequestBodyConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .bearerTokenResolver(allowRequestBody()) + .jwt(); + // @formatter:on + } + + private BearerTokenResolver allowRequestBody() { + DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); + resolver.setAllowFormEncodedBodyParameter(true); + return resolver; + } + } + + @EnableWebSecurity + static class AllowBearerTokenAsQueryParameterConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt(); + // @formatter:on + } + + @Bean + BearerTokenResolver allowQueryParameter() { + DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); + resolver.setAllowUriQueryParameter(true); + return resolver; + } + } + + @EnableWebSecurity + static class MultipleBearerTokenResolverBeansConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt(); + // @formatter:on + } + + @Bean + BearerTokenResolver resolverOne() { + DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); + resolver.setAllowUriQueryParameter(true); + return resolver; + } + + @Bean + BearerTokenResolver resolverTwo() { + DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); + resolver.setAllowFormEncodedBodyParameter(true); + return resolver; + } + } + + @EnableWebSecurity + static class CustomJwtDecoderOnDsl extends WebSecurityConfigurerAdapter { + JwtDecoder decoder = mock(JwtDecoder.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .decoder(decoder()); + // @formatter:on + } + + JwtDecoder decoder() { + return this.decoder; + } + } + + @EnableWebSecurity + static class CustomJwtDecoderAsBean extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt(); + // @formatter:on + } + + @Bean + public JwtDecoder decoder() { + return mock(JwtDecoder.class); + } + } + + @EnableWebSecurity + static class CustomJwtValidatorConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri}") String uri; + + private final OAuth2TokenValidator jwtValidator = mock(OAuth2TokenValidator.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + NimbusJwtDecoderJwkSupport jwtDecoder = + new NimbusJwtDecoderJwkSupport(this.uri); + jwtDecoder.setJwtValidator(this.jwtValidator); + + // @formatter:off + http + .oauth2() + .resourceServer() + .jwt() + .decoder(jwtDecoder); + // @formatter:on + } + + public OAuth2TokenValidator getJwtValidator() { + return this.jwtValidator; + } + } + + @EnableWebSecurity + static class UnexpiredJwtClockSkewConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + Clock nearlyAnHourFromTokenExpiry = + Clock.fixed(Instant.ofEpochMilli(4687181540000L), ZoneId.systemDefault()); + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofHours(1)); + jwtValidator.setClock(nearlyAnHourFromTokenExpiry); + + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(this.uri); + jwtDecoder.setJwtValidator(jwtValidator); + + // @formatter:off + http + .oauth2() + .resourceServer() + .jwt() + .decoder(jwtDecoder); + // @formatter:on + } + } + + @EnableWebSecurity + static class ExpiredJwtClockSkewConfig extends WebSecurityConfigurerAdapter { + @Value("${mock.jwk-set-uri}") String uri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + Clock justOverOneHourAfterExpiry = + Clock.fixed(Instant.ofEpochMilli(4687181595000L), ZoneId.systemDefault()); + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofHours(1)); + jwtValidator.setClock(justOverOneHourAfterExpiry); + + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(this.uri); + jwtDecoder.setJwtValidator(jwtValidator); + + // @formatter:off + http + .oauth2() + .resourceServer() + .jwt() + .decoder(jwtDecoder); + // @formatter:on + } + } + + @Configuration + static class JwtDecoderConfig { + @Bean + public JwtDecoder jwtDecoder() { + return mock(JwtDecoder.class); + } + } + + @RestController + static class BasicController { + @GetMapping("/") + public String get() { + return "ok"; + } + + @PostMapping("/post") + public String post() { + return "post"; + } + + @RequestMapping(value = "/authenticated", method = { GET, POST }) + public String authenticated(@AuthenticationPrincipal Authentication authentication) { + return authentication.getName(); + } + + @GetMapping("/requires-read-scope") + public String requiresReadScope(@AuthenticationPrincipal JwtAuthenticationToken token) { + return token.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .filter(auth -> auth.endsWith("message:read")) + .findFirst().orElse(null); + } + + @GetMapping("/ms-requires-read-scope") + @PreAuthorize("hasAuthority('SCOPE_message:read')") + public String msRequiresReadScope(@AuthenticationPrincipal JwtAuthenticationToken token) { + return requiresReadScope(token); + } + + @GetMapping("/ms-deny") + @PreAuthorize("denyAll") + public String deny() { + return "hmm, that's odd"; + } + } + + @Configuration + static class WebServerConfig implements BeanPostProcessor { + private final MockWebServer server = new MockWebServer(); + + @PreDestroy + public void shutdown() throws IOException { + this.server.shutdown(); + } + + @Bean + public MockWebServer authz() { + return this.server; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof WebSecurityConfigurerAdapter) { + Field f = ReflectionUtils.findField(bean.getClass(), field -> + field.getAnnotation(Value.class) != null); + if (f != null) { + ReflectionUtils.setField(f, bean, this.server.url("/.well-known/jwks.json").toString()); + } + } + return null; + } + } + + private static class BearerTokenRequestPostProcessor implements RequestPostProcessor { + private boolean asRequestParameter; + + private String token; + + public BearerTokenRequestPostProcessor(String token) { + this.token = token; + } + + public BearerTokenRequestPostProcessor asParam() { + this.asRequestParameter = true; + return this; + } + + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + if (this.asRequestParameter) { + request.setParameter("access_token", this.token); + } else { + request.addHeader("Authorization", "Bearer " + this.token); + } + + return request; + } + } + + private static BearerTokenRequestPostProcessor bearerToken(String token) { + return new BearerTokenRequestPostProcessor(token); + } + + private static ResultMatcher invalidRequestHeader(String message) { + return header().string(HttpHeaders.WWW_AUTHENTICATE, + AllOf.allOf( + new StringStartsWith("Bearer " + + "error=\"invalid_request\", " + + "error_description=\""), + new StringContains(message), + new StringEndsWith(", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"") + ) + ); + } + + private static ResultMatcher invalidTokenHeader(String message) { + return header().string(HttpHeaders.WWW_AUTHENTICATE, + AllOf.allOf( + new StringStartsWith("Bearer " + + "error=\"invalid_token\", " + + "error_description=\""), + new StringContains(message), + new StringEndsWith(", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"") + ) + ); + } + + private static ResultMatcher insufficientScopeHeader(String scope) { + return header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer " + + "error=\"insufficient_scope\"" + + ", error_description=\"The token provided has insufficient scope [" + scope + "] for this request\"" + + ", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"" + + (StringUtils.hasText(scope) ? ", scope=\"" + scope + "\"" : "")); + } + + private String token(String name) throws IOException { + return resource(name + ".token"); + } + + private MockResponse jwks(String name) throws IOException { + String response = resource(name + ".jwks"); + return new MockResponse() + .setResponseCode(200) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .setBody(response); + } + + private String resource(String suffix) throws IOException { + String name = this.getClass().getSimpleName() + "-" + suffix; + ClassPathResource resource = new ClassPathResource(name, this.getClass()); + try ( BufferedReader reader = new BufferedReader(new FileReader(resource.getFile())) ) { + return reader.lines().collect(Collectors.joining()); + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java index 71b7297aa68..2654355755a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java @@ -16,14 +16,6 @@ package org.springframework.security.config.annotation.web.reactive; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; - -import java.nio.charset.StandardCharsets; -import java.security.Principal; - import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -69,9 +61,14 @@ import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.result.view.AbstractView; - import reactor.core.publisher.Mono; +import java.nio.charset.StandardCharsets; +import java.security.Principal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf; + /** * @author Rob Winch * @since 5.0 @@ -123,11 +120,10 @@ public void authenticateWhenBasicThenNoSession() { WebTestClient client = WebTestClientBuilder .bindToWebFilters(this.springSecurityFilterChain) - .filter(basicAuthentication()) .build(); FluxExchangeResult result = client.get() - .attributes(basicAuthenticationCredentials("user", "password")) + .headers(headers -> headers.setBasicAuth("user", "password")) .exchange() .expectStatus() .isOk() @@ -172,13 +168,12 @@ public void defaultPopulatesReactorContextWhenAuthenticating() { .flatMap( principal -> exchange.getResponse() .writeWith(Mono.just(toDataBuffer(principal.getName())))) ) - .filter(basicAuthentication()) .build(); client .get() .uri("/") - .attributes(basicAuthenticationCredentials("user", "password")) + .headers(headers -> headers.setBasicAuth("user", "password")) .exchange() .expectStatus().isOk() .expectBody(String.class).consumeWith( result -> assertThat(result.getResponseBody()).isEqualTo("user")); @@ -209,13 +204,12 @@ public void passwordEncoderBeanIsUsed() { .flatMap( principal -> exchange.getResponse() .writeWith(Mono.just(toDataBuffer(principal.getName())))) ) - .filter(basicAuthentication()) .build(); client .get() .uri("/") - .attributes(basicAuthenticationCredentials("user", "password")) + .headers(headers -> headers.setBasicAuth("user", "password")) .exchange() .expectStatus().isOk() .expectBody(String.class).consumeWith( result -> assertThat(result.getResponseBody()).isEqualTo("user")); @@ -238,6 +232,34 @@ public static PasswordEncoder passwordEncoder() { } } + @Test + public void passwordUpdateManagerUsed() { + this.spring.register(MapReactiveUserDetailsServiceConfig.class).autowire(); + WebTestClient client = WebTestClientBuilder.bindToWebFilters(this.springSecurityFilterChain).build(); + + client + .get() + .uri("/") + .headers(h -> h.setBasicAuth("user", "password")) + .exchange() + .expectStatus().isOk(); + + ReactiveUserDetailsService users = this.spring.getContext().getBean(ReactiveUserDetailsService.class); + assertThat(users.findByUsername("user").block().getPassword()).startsWith("{bcrypt}"); + } + + @EnableWebFluxSecurity + static class MapReactiveUserDetailsServiceConfig { + @Bean + public MapReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsService(User.withUsername("user") + .password("{noop}password") + .roles("USER") + .build() + ); + } + } + @Test public void formLoginWorks() { this.spring.register(Config.class).autowire(); 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 40ff4869f40..b37cb59fdca 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 @@ -493,7 +493,7 @@ private MockHttpServletRequest websocketHttpRequest(String mapping) { } private MockHttpServletRequest sockjsHttpRequest(String mapping) { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.setMethod("GET"); request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/289/tpyx6mde/websocket"); diff --git a/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsJcTests.java b/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsJcTests.java index f35139dd99f..8b5ad382245 100644 --- a/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsJcTests.java +++ b/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsJcTests.java @@ -65,7 +65,7 @@ public class GrantedAuthorityDefaultsJcTests { public void setup() { setup("USER"); - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); request.setMethod("GET"); response = new MockHttpServletResponse(); chain = new MockFilterChain(); diff --git a/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsXmlTests.java b/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsXmlTests.java index c36341ab506..6eac7b9254c 100644 --- a/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsXmlTests.java +++ b/config/src/test/java/org/springframework/security/config/core/GrantedAuthorityDefaultsXmlTests.java @@ -58,7 +58,7 @@ public class GrantedAuthorityDefaultsXmlTests { public void setup() { setup("USER"); - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); request.setMethod("GET"); response = new MockHttpServletResponse(); chain = new MockFilterChain(); 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 11a36966e6f..768bdfabac5 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 @@ -50,7 +50,7 @@ public class XsdDocumentedTests { String referenceLocation = "../docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc"; String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd"; - String schemaDocumentLocation = "org/springframework/security/config/spring-security-5.0.xsd"; + String schemaDocumentLocation = "org/springframework/security/config/spring-security-5.1.xsd"; XmlSupport xml = new XmlSupport(); @@ -142,7 +142,7 @@ public void sizeWhenReadingFilesystemThenIsCorrectNumberOfSchemaFiles() String[] schemas = resource.getFile().getParentFile().list((dir, name) -> name.endsWith(".xsd")); - assertThat(schemas.length).isEqualTo(12) + assertThat(schemas.length).isEqualTo(13) .withFailMessage("the count is equal to 12, if not then schemaDocument needs updating"); } diff --git a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java index 31d166252e0..755a14769fa 100644 --- a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java @@ -528,7 +528,8 @@ public void logoutWhenDefaultConfigurationThenDisabled() this.xml("CsrfEnabled") ).autowire(); - this.mvc.perform(get("/logout")).andExpect(status().isNotFound()); + this.mvc.perform(get("/logout")) + .andExpect(status().isOk()); // renders form to log out but does not perform a redirect // still logged in this.mvc.perform(get("/authenticated")).andExpect(status().isOk()); diff --git a/config/src/test/java/org/springframework/security/config/http/FilterSecurityMetadataSourceBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/FilterSecurityMetadataSourceBeanDefinitionParserTests.java index 7b8e1ccd9d0..23a0691fb95 100644 --- a/config/src/test/java/org/springframework/security/config/http/FilterSecurityMetadataSourceBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/FilterSecurityMetadataSourceBeanDefinitionParserTests.java @@ -123,7 +123,7 @@ public void parsingInterceptUrlServletPathFails() { } private FilterInvocation createFilterInvocation(String path, String method) { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.setRequestURI(null); request.setMethod(method); 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 86a3290c65a..e13c68b9ea9 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 @@ -22,6 +22,7 @@ import org.springframework.security.web.WebAttributes; import org.springframework.test.web.servlet.MockMvc; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.core.IsNot.not; import static org.hamcrest.core.IsNull.nullValue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -54,18 +55,46 @@ public void getLoginWhenAutoConfigThenShowsDefaultLoginPage() this.spring.configLocations(this.xml("Simple")).autowire(); String expectedContent = - "Login Page\n" + - "

        Login with Username and Password

        \n" + - "\n" + - " \n" + - " \n" + - " \n" + - "
        User:
        Password:
        \n" + - "
        "; + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Please sign in\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
        \n" + + "
        \n" + + " \n" + + "

        \n" + + " \n" + + " \n" + + "

        \n" + + "

        \n" + + " \n" + + " \n" + + "

        \n" + + " \n" + + "
        \n" + + ""; this.mvc.perform(get("/login")).andExpect(content().string(expectedContent)); } + @Test + public void getLogoutWhenAutoConfigThenShowsDefaultLogoutPage() + throws Exception { + + this.spring.configLocations(this.xml("AutoConfig")).autowire(); + + this.mvc.perform(get("/logout")) + .andExpect(content().string(containsString("action=\"/logout\""))); + } + @Test public void getLoginWhenConfiguredWithCustomAttributesThenLoginPageReflects() throws Exception { @@ -73,16 +102,35 @@ public void getLoginWhenConfiguredWithCustomAttributesThenLoginPageReflects() this.spring.configLocations(this.xml("WithCustomAttributes")).autowire(); String expectedContent = - "Login Page\n" + - "

        Login with Username and Password

        \n" + - "\n" + - " \n" + - " \n" + - " \n" + - "
        User:
        Password:
        \n" + - "
        "; + "\n" + + "\n" + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Please sign in\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
        \n" + + "
        \n" + + " \n" + + "

        \n" + + " \n" + + " \n" + + "

        \n" + + "

        \n" + + " \n" + + " \n" + + "

        \n" + + " \n" + + "
        \n" + + ""; this.mvc.perform(get("/login")).andExpect(content().string(expectedContent)); + + this.mvc.perform(get("/logout")).andExpect(status().is3xxRedirection()); } @Test @@ -92,19 +140,38 @@ public void getLoginWhenConfiguredForOpenIdThenLoginPageReflects() this.spring.configLocations(this.xml("WithOpenId")).autowire(); String expectedContent = - "Login Page\n" + - "

        Login with Username and Password

        \n" + - "\n" + - " \n" + - " \n" + - " \n" + - "
        User:
        Password:
        \n" + - "

        Login with OpenID Identity

        \n" + - "\n" + - " \n" + - " \n" + - "
        Identity:
        \n" + - "
        "; + "\n" + "\n" + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Please sign in\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
        \n" + + "
        \n" + + " \n" + + "

        \n" + + " \n" + + " \n" + + "

        \n" + + "

        \n" + + " \n" + + " \n" + + "

        \n" + + " \n" + + "
        \n" + + "
        \n" + + " \n" + + "

        \n" + + " \n" + + " \n" + + "

        \n" + + " \n" + + "
        \n" + + ""; this.mvc.perform(get("/login")).andExpect(content().string(expectedContent)); } @@ -116,19 +183,38 @@ public void getLoginWhenConfiguredForOpenIdWithCustomAttributesThenLoginPageRefl this.spring.configLocations(this.xml("WithOpenIdCustomAttributes")).autowire(); String expectedContent = - "Login Page\n" + - "

        Login with Username and Password

        \n" + - "\n" + - " \n" + - " \n" + - " \n" + - "
        User:
        Password:
        \n" + - "

        Login with OpenID Identity

        \n" + - "\n" + - " \n" + - " \n" + - "
        Identity:
        \n" + - "
        "; + "\n" + "\n" + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Please sign in\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
        \n" + + "
        \n" + + " \n" + + "

        \n" + + " \n" + + " \n" + + "

        \n" + + "

        \n" + + " \n" + + " \n" + + "

        \n" + + " \n" + + "
        \n" + + "
        \n" + + " \n" + + "

        \n" + + " \n" + + " \n" + + "

        \n" + + " \n" + + "
        \n" + + ""; this.mvc.perform(get("/login")).andExpect(content().string(expectedContent)); } diff --git a/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java new file mode 100644 index 00000000000..dfc2e7a34d9 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/HttpConfigTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 org.apache.http.HttpStatus; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.test.web.servlet.MockMvc; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Rob Winch + * @author Josh Cummings + */ +public class HttpConfigTests { + + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/HttpConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void getWhenUsingMinimalConfigurationThenRedirectsToLogin() + throws Exception { + + this.spring.configLocations(this.xml("Minimal")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + public void getWhenUsingMinimalConfigurationThenPreventsSessionAsUrlParameter() + throws Exception { + + this.spring.configLocations(this.xml("Minimal")).autowire(); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChainProxy proxy = this.spring.getContext().getBean(FilterChainProxy.class); + + proxy.doFilter( + request, + new EncodeUrlDenyingHttpServletResponseWrapper(response), + (req, resp) -> {}); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/login"); + } + + private static class EncodeUrlDenyingHttpServletResponseWrapper + extends HttpServletResponseWrapper { + + public EncodeUrlDenyingHttpServletResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public String encodeURL(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeRedirectURL(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeUrl(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeRedirectUrl(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/HttpCorsConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpCorsConfigTests.java new file mode 100644 index 00000000000..52c3245a120 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/HttpCorsConfigTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; +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.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Rob Winch + * @author Tim Ysewyn + * @author Josh Cummings + */ +public class HttpCorsConfigTests { + + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/HttpCorsConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void autowireWhenMissingMvcThenGivesInformativeError() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("RequiresMvc")).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasMessageContaining("Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext"); + } + + @Test + public void getWhenUsingCorsThenDoesSpringSecurityCorsHandshake() + throws Exception { + + this.spring.configLocations(this.xml("WithCors")).autowire(); + + this.mvc.perform(get("/").with(this.approved())) + .andExpect(corsResponseHeaders()) + .andExpect((status().isIAmATeapot())); + + this.mvc.perform(options("/").with(this.preflight())) + .andExpect(corsResponseHeaders()) + .andExpect(status().isOk()); + } + + @Test + public void getWhenUsingCustomCorsConfigurationSourceThenDoesSpringSecurityCorsHandshake() + throws Exception { + + this.spring.configLocations(this.xml("WithCorsConfigurationSource")).autowire(); + + this.mvc.perform(get("/").with(this.approved())) + .andExpect(corsResponseHeaders()) + .andExpect((status().isIAmATeapot())); + + this.mvc.perform(options("/").with(this.preflight())) + .andExpect(corsResponseHeaders()) + .andExpect(status().isOk()); + } + + @Test + public void getWhenUsingCustomCorsFilterThenDoesSPringSecurityCorsHandshake() + throws Exception { + + this.spring.configLocations(this.xml("WithCorsFilter")).autowire(); + + this.mvc.perform(get("/").with(this.approved())) + .andExpect(corsResponseHeaders()) + .andExpect((status().isIAmATeapot())); + + this.mvc.perform(options("/").with(this.preflight())) + .andExpect(corsResponseHeaders()) + .andExpect(status().isOk()); + } + + @RestController + @CrossOrigin(methods = { + RequestMethod.GET, RequestMethod.POST + }) + static class CorsController { + @RequestMapping("/") + String hello() { + return "Hello"; + } + } + + static class MyCorsConfigurationSource extends UrlBasedCorsConfigurationSource { + MyCorsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList(RequestMethod.GET.name(), RequestMethod.POST.name())); + + super.registerCorsConfiguration( + "/**", + configuration); + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } + + private RequestPostProcessor preflight() { + return cors(true); + } + + private RequestPostProcessor approved() { + return cors(false); + } + + private RequestPostProcessor cors(boolean preflight) { + return (request) -> { + request.addHeader(HttpHeaders.ORIGIN, "https://example.com"); + + if ( preflight ) { + request.setMethod(HttpMethod.OPTIONS.name()); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name()); + } + + return request; + }; + } + + private ResultMatcher corsResponseHeaders() { + return result -> { + header().exists("Access-Control-Allow-Origin").match(result); + header().exists("X-Content-Type-Options").match(result); + }; + } + +} diff --git a/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java new file mode 100644 index 00000000000..6e34b5f5bc6 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java @@ -0,0 +1,776 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 com.google.common.collect.ImmutableMap; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Rob Winch + * @author Tim Ysewyn + * @author Josh Cummings + */ +public class HttpHeadersConfigTests { + + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/HttpHeadersConfigTests"; + + static final Map defaultHeaders = + ImmutableMap.builder() + .put("X-Content-Type-Options", "nosniff") + .put("X-Frame-Options", "DENY") + .put("Strict-Transport-Security", "max-age=31536000 ; includeSubDomains") + .put("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate") + .put("Expires", "0") + .put("Pragma", "no-cache") + .put("X-XSS-Protection", "1; mode=block") + .build(); + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void requestWhenHeadersDisabledThenResponseExcludesAllSecureHeaders() + throws Exception { + + this.spring.configLocations(this.xml("HeadersDisabled")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()); + } + + @Test + public void configureWhenHeadersDisabledHavingChildElementThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("HeadersDisabledHavingChildElement")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("Cannot specify with child elements"); + } + + @Test + public void requestWhenHeadersEnabledThenResponseContainsAllSecureHeaders() + throws Exception { + + this.spring.configLocations(this.xml("DefaultConfig")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includesDefaults()); + } + + @Test + public void requestWhenHeadersElementUsedThenResponseContainsAllSecureHeaders() + throws Exception { + + this.spring.configLocations(this.xml("HeadersEnabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includesDefaults()); + } + + @Test + public void requestWhenFrameOptionsConfiguredThenIncludesHeader() + throws Exception { + + Map headers = new HashMap(defaultHeaders); + headers.put("X-Frame-Options", "SAMEORIGIN"); + + this.spring.configLocations(this.xml("WithFrameOptions")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(headers)); + } + + // -- defaults disabled + + /** + * gh-3986 + */ + @Test + public void requestWhenDefaultsDisabledWithNoOverrideThenExcludesAllSecureHeaders() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithNoOverride")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingContentTypeOptionsThenDefaultsToNoSniff() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-Content-Type-Options"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithContentTypeOptions")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Content-Type-Options", "nosniff")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenUsingFrameOptionsThenDefaultsToDeny() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-Frame-Options"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptions")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "DENY")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenUsingFrameOptionsDenyThenRespondsWithDeny() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-Frame-Options"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptionsDeny")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "DENY")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenUsingFrameOptionsSameOriginThenRespondsWithSameOrigin() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-Frame-Options"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptionsSameOrigin")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "SAMEORIGIN")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void configureWhenUsingFrameOptionsAllowFromNoOriginThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptionsAllowFromNoOrigin")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("Strategy requires a 'value' to be set."); // FIXME better error message? + } + + @Test + public void configureWhenUsingFrameOptionsAllowFromBlankOriginThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptionsAllowFromBlankOrigin")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("Strategy requires a 'value' to be set."); // FIXME better error message? + } + + @Test + public void requestWhenUsingFrameOptionsAllowFromThenRespondsWithAllowFrom() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-Frame-Options"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptionsAllowFrom")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "ALLOW-FROM https://example.org")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenUsingFrameOptionsAllowFromWhitelistThenRespondsWithAllowFrom() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-Frame-Options"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithFrameOptionsAllowFromWhitelist")).autowire(); + + this.mvc.perform(get("/").param("from", "https://example.org")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "ALLOW-FROM https://example.org")) + .andExpect(excludes(excludedHeaders)); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-Frame-Options", "DENY")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenUsingCustomHeaderThenRespondsWithThatHeader() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithCustomHeader")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("a", "b")) + .andExpect(header().string("c", "d")) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingCustomHeaderWriterThenRespondsWithThatHeader() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithCustomHeaderWriter")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("abc", "def")) + .andExpect(excludesDefaults()); + } + + @Test + public void configureWhenUsingCustomHeaderNameOnlyThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithOnlyHeaderName")).autowire()) + .isInstanceOf(BeanCreationException.class); + } + + @Test + public void configureWhenUsingCustomHeaderValueOnlyThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithOnlyHeaderValue")).autowire()) + .isInstanceOf(BeanCreationException.class); + } + + @Test + public void requestWhenUsingXssProtectionThenDefaultsToModeBlock() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-XSS-Protection"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithXssProtection")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-XSS-Protection", "1; mode=block")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenEnablingXssProtectionThenDefaultsToModeBlock() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-XSS-Protection"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithXssProtectionEnabled")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-XSS-Protection", "1; mode=block")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void requestWhenDisablingXssProtectionThenDefaultsToZero() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("X-XSS-Protection"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithXssProtectionDisabled")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("X-XSS-Protection", "0")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void configureWhenXssProtectionDisabledAndBlockSetThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithXssProtectionDisabledAndBlockSet")).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasMessageContaining("Cannot set block to true with enabled false"); + } + + @Test + public void requestWhenUsingCacheControlThenRespondsWithCorrespondingHeaders() + throws Exception { + + Map includedHeaders = ImmutableMap.builder() + .put("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate") + .put("Expires", "0") + .put("Pragma", "no-cache") + .build(); + + this.spring.configLocations(this.xml("DefaultsDisabledWithCacheControl")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(includes(includedHeaders)); + } + + @Test + public void requestWhenUsingHstsThenRespondsWithHstsHeader() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("Strict-Transport-Security"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithHsts")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string("Strict-Transport-Security", "max-age=31536000 ; includeSubDomains")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void insecureRequestWhenUsingHstsThenExcludesHstsHeader() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithHsts")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()); + } + + @Test + public void insecureRequestWhenUsingCustomHstsRequestMatcherThenIncludesHstsHeader() + throws Exception { + + Set excludedHeaders = new HashSet<>(defaultHeaders.keySet()); + excludedHeaders.remove("Strict-Transport-Security"); + + this.spring.configLocations(this.xml("DefaultsDisabledWithCustomHstsRequestMatcher")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().string("Strict-Transport-Security", "max-age=1")) + .andExpect(excludes(excludedHeaders)); + } + + @Test + public void configureWhenUsingHpkpWithoutPinsThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithEmptyHpkp")).autowire()) + .isInstanceOf(XmlBeanDefinitionStoreException.class) + .hasMessageContaining("The content of element 'hpkp' is not complete"); + } + + @Test + public void configureWhenUsingHpkpWithEmptyPinsThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("DefaultsDisabledWithEmptyPins")).autowire()) + .isInstanceOf(XmlBeanDefinitionStoreException.class) + .hasMessageContaining("The content of element 'pins' is not complete"); + } + + @Test + public void requestWhenUsingHpkpThenIncludesHpkpHeader() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkp")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string( + "Public-Key-Pins-Report-Only", + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"")) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingHpkpDefaultsThenIncludesHpkpHeaderUsingSha256() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkpDefaults")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string( + "Public-Key-Pins-Report-Only", + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"")) + .andExpect(excludesDefaults()); + } + + @Test + public void insecureRequestWhenUsingHpkpThenExcludesHpkpHeader() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkpDefaults")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(header().doesNotExist("Public-Key-Pins-Report-Only")) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingHpkpCustomMaxAgeThenIncludesHpkpHeaderAccordingly() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkpMaxAge")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string( + "Public-Key-Pins-Report-Only", + "max-age=604800 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"")) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingHpkpReportThenIncludesHpkpHeaderAccordingly() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkpReport")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string( + "Public-Key-Pins", + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"")) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingHpkpIncludeSubdomainsThenIncludesHpkpHeaderAccordingly() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkpIncludeSubdomains")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string( + "Public-Key-Pins-Report-Only", + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\" ; includeSubDomains")) + .andExpect(excludesDefaults()); + } + + @Test + public void requestWhenUsingHpkpReportUriThenIncludesHpkpHeaderAccordingly() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithHpkpReportUri")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(header().string( + "Public-Key-Pins-Report-Only", + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\" ; report-uri=\"http://example.net/pkp-report\"")) + .andExpect(excludesDefaults()); + } + + // -- single-header disabled + + @Test + public void requestWhenCacheControlDisabledThenExcludesHeader() + throws Exception { + + Collection cacheControl = Arrays.asList("Cache-Control", "Expires", "Pragma"); + Map allButCacheControl = remove(defaultHeaders, cacheControl); + + this.spring.configLocations(this.xml("CacheControlDisabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(allButCacheControl)) + .andExpect(excludes(cacheControl)); + } + + @Test + public void requestWhenContentTypeOptionsDisabledThenExcludesHeader() + throws Exception { + + Collection contentTypeOptions = Arrays.asList("X-Content-Type-Options"); + Map allButContentTypeOptions = remove(defaultHeaders, contentTypeOptions); + + this.spring.configLocations(this.xml("ContentTypeOptionsDisabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(allButContentTypeOptions)) + .andExpect(excludes(contentTypeOptions)); + } + + @Test + public void requestWhenHstsDisabledThenExcludesHeader() + throws Exception { + + Collection hsts = Arrays.asList("Strict-Transport-Security"); + Map allButHsts = remove(defaultHeaders, hsts); + + this.spring.configLocations(this.xml("HstsDisabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(allButHsts)) + .andExpect(excludes(hsts)); + } + + @Test + public void requestWhenHpkpDisabledThenExcludesHeader() + throws Exception { + + this.spring.configLocations(this.xml("HpkpDisabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includesDefaults()); + } + + @Test + public void requestWhenFrameOptionsDisabledThenExcludesHeader() + throws Exception { + + Collection frameOptions = Arrays.asList("X-Frame-Options"); + Map allButFrameOptions = remove(defaultHeaders, frameOptions); + + this.spring.configLocations(this.xml("FrameOptionsDisabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(allButFrameOptions)) + .andExpect(excludes(frameOptions)); + } + + @Test + public void requestWhenXssProtectionDisabledThenExcludesHeader() + throws Exception { + + Collection xssProtection = Arrays.asList("X-XSS-Protection"); + Map allButXssProtection = remove(defaultHeaders, xssProtection); + + this.spring.configLocations(this.xml("XssProtectionDisabled")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(allButXssProtection)) + .andExpect(excludes(xssProtection)); + } + + // --- disable error handling --- + + @Test + public void configureWhenHstsDisabledAndIncludeSubdomainsSpecifiedThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("HstsDisabledSpecifyingIncludeSubdomains")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("include-subdomains"); + } + + @Test + public void configureWhenHstsDisabledAndMaxAgeSpecifiedThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("HstsDisabledSpecifyingMaxAge")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("max-age"); + } + + @Test + public void configureWhenHstsDisabledAndRequestMatcherSpecifiedThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("HstsDisabledSpecifyingRequestMatcher")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("request-matcher-ref"); + } + + @Test + public void configureWhenXssProtectionDisabledAndEnabledThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("XssProtectionDisabledAndEnabled")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("enabled"); + } + + @Test + public void configureWhenXssProtectionDisabledAndBlockSpecifiedThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("XssProtectionDisabledSpecifyingBlock")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("block"); + } + + @Test + public void configureWhenFrameOptionsDisabledAndPolicySpecifiedThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("FrameOptionsDisabledSpecifyingPolicy")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("policy"); + } + + @Test + public void requestWhenContentSecurityPolicyDirectivesConfiguredThenIncludesDirectives() + throws Exception { + + Map includedHeaders = new HashMap<>(defaultHeaders); + includedHeaders.put("Content-Security-Policy", "default-src 'self'"); + + this.spring.configLocations(this.xml("ContentSecurityPolicyWithPolicyDirectives")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(includedHeaders)); + } + + @Test + public void requestWhenHeadersDisabledAndContentSecurityPolicyConfiguredThenExcludesHeader() + throws Exception { + + this.spring.configLocations(this.xml("HeadersDisabledWithContentSecurityPolicy")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(excludes("Content-Security-Policy")); + } + + @Test + public void requestWhenDefaultsDisabledAndContentSecurityPolicyConfiguredThenIncludesHeader() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithContentSecurityPolicy")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Content-Security-Policy", "default-src 'self'")); + } + + @Test + public void configureWhenContentSecurityPolicyConfiguredWithEmptyDirectivesThenAutowireFails() { + assertThatThrownBy(() -> + this.spring.configLocations(this.xml("ContentSecurityPolicyWithEmptyDirectives")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @Test + public void requestWhenContentSecurityPolicyConfiguredWithReportOnlyThenIncludesReportOnlyHeader() + throws Exception { + + Map includedHeaders = new HashMap<>(defaultHeaders); + includedHeaders.put("Content-Security-Policy-Report-Only", "default-src https:; report-uri https://example.org/"); + + this.spring.configLocations(this.xml("ContentSecurityPolicyWithReportOnly")).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(status().isOk()) + .andExpect(includes(includedHeaders)); + } + + @Test + public void requestWhenReferrerPolicyConfiguredThenResponseDefaultsToNoReferrer() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithReferrerPolicy")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Referrer-Policy", "no-referrer")); + } + + @Test + public void requestWhenReferrerPolicyConfiguredWithSameOriginThenRespondsWithSameOrigin() + throws Exception { + + this.spring.configLocations(this.xml("DefaultsDisabledWithReferrerPolicySameOrigin")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Referrer-Policy", "same-origin")); + } + + @RestController + public static class SimpleController { + @GetMapping("/") + public String ok() { return "ok"; } + } + + private static ResultMatcher includesDefaults() { + return includes(defaultHeaders); + } + + private static ResultMatcher includes(Map headers) { + return result -> { + for ( Map.Entry header : headers.entrySet() ) { + header().string(header.getKey(), header.getValue()).match(result); + } + }; + } + + private static ResultMatcher excludesDefaults() { + return excludes(defaultHeaders.keySet()); + } + + private static ResultMatcher excludes(Collection headers) { + return result -> { + for ( String name : headers ) { + header().doesNotExist(name).match(result); + } + }; + } + + private static ResultMatcher excludes(String... headers) { + return excludes(Arrays.asList(headers)); + } + + private static Map remove(Map map, Collection keys) { + Map copy = new HashMap<>(map); + + for ( K key : keys ) { + copy.remove(key); + } + + return copy; + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/InterceptUrlConfigTests.java b/config/src/test/java/org/springframework/security/config/http/InterceptUrlConfigTests.java new file mode 100644 index 00000000000..2de3e5912dd --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/InterceptUrlConfigTests.java @@ -0,0 +1,293 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 org.junit.Rule; +import org.junit.Test; +import org.mockito.stubbing.Answer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.ConfigurableWebApplicationContext; + +import javax.servlet.ServletRegistration; +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +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.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Rob Winch + * @author Josh Cummings + */ +public class InterceptUrlConfigTests { + + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/InterceptUrlConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + /** + * sec-2256 + */ + @Test + public void requestWhenMethodIsSpecifiedThenItIsNotGivenPriority() + throws Exception { + + this.spring.configLocations(this.xml("Sec2256")).autowire(); + + this.mvc.perform(post("/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + } + + /** + * sec-2355 + */ + @Test + public void requestWhenUsingPatchThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("PatchMethod")).autowire(); + + this.mvc.perform(get("/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(patch("/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + + this.mvc.perform(patch("/path") + .with(httpBasic("admin", "password"))) + .andExpect(status().isOk()); + + } + + @Test + public void requestWhenUsingHasAnyRoleThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("HasAnyRole")).autowire(); + + this.mvc.perform(get("/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/path") + .with(httpBasic("admin", "password"))) + .andExpect(status().isForbidden()); + } + + /** + * sec-2059 + */ + @Test + public void requestWhenUsingPathVariablesThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("PathVariables")).autowire(); + + this.mvc.perform(get("/path/user/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/path/otheruser/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + + this.mvc.perform(get("/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + } + + /** + * gh-3786 + */ + @Test + public void requestWhenUsingCamelCasePathVariablesThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("CamelCasePathVariables")).autowire(); + + this.mvc.perform(get("/path/user/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/path/otheruser/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + + this.mvc.perform(get("/PATH/user/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + } + + /** + * sec-2059 + */ + @Test + public void requestWhenUsingPathVariablesAndTypeConversionThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("TypeConversionPathVariables")).autowire(); + + this.mvc.perform(get("/path/1/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/path/2/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + + } + + @Test + public void requestWhenUsingMvcMatchersThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("MvcMatchers")).autowire(); + + this.mvc.perform(get("/path")) + .andExpect(status().isUnauthorized()); + + this.mvc.perform(get("/path.html")) + .andExpect(status().isUnauthorized()); + + this.mvc.perform(get("/path/")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void requestWhenUsingMvcMatchersAndPathVariablesThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("MvcMatchersPathVariables")).autowire(); + + this.mvc.perform(get("/path/user/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/path/otheruser/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + + this.mvc.perform(get("/PATH/user/path") + .with(httpBasic("user", "password"))) + .andExpect(status().isForbidden()); + } + + @Test + public void requestWhenUsingMvcMatchersAndServletPathThenAuthorizesRequestsAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("MvcMatchersServletPath")).autowire(); + + MockServletContext servletContext = mockServletContext("/spring"); + ConfigurableWebApplicationContext context = + (ConfigurableWebApplicationContext) this.spring.getContext(); + context.setServletContext(servletContext); + + this.mvc.perform(get("/spring/path").servletPath("/spring")) + .andExpect(status().isUnauthorized()); + + this.mvc.perform(get("/spring/path.html").servletPath("/spring")) + .andExpect(status().isUnauthorized()); + + this.mvc.perform(get("/spring/path/").servletPath("/spring")) + .andExpect(status().isUnauthorized()); + + } + + @Test + public void configureWhenUsingAntMatcherAndServletPathThenThrowsException() { + assertThatCode(() -> this.spring.configLocations(this.xml("AntMatcherServletPath")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @Test + public void configureWhenUsingRegexMatcherAndServletPathThenThrowsException() { + assertThatCode(() -> this.spring.configLocations(this.xml("RegexMatcherServletPath")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @Test + public void configureWhenUsingCiRegexMatcherAndServletPathThenThrowsException() { + assertThatCode(() -> this.spring.configLocations(this.xml("CiRegexMatcherServletPath")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @Test + public void configureWhenUsingDefaultMatcherAndServletPathThenThrowsException() { + assertThatCode(() -> this.spring.configLocations(this.xml("DefaultMatcherServletPath")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @RestController + static class PathController { + @RequestMapping("/path") + public String path() { + return "path"; + } + + @RequestMapping("/path/{un}/path") + public String path(@PathVariable("un") String name) { + return name; + } + } + + public static class Id { + public boolean isOne(int i) { + return i == 1; + } + } + + private MockServletContext mockServletContext(String servletPath) { + MockServletContext servletContext = spy(new MockServletContext()); + final ServletRegistration registration = mock(ServletRegistration.class); + when(registration.getMappings()).thenReturn(Collections.singleton(servletPath)); + Answer> answer = invocation -> + Collections.singletonMap("spring", registration); + when(servletContext.getServletRegistrations()).thenAnswer(answer); + return servletContext; + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/MultiHttpBlockConfigTests.java b/config/src/test/java/org/springframework/security/config/http/MultiHttpBlockConfigTests.java new file mode 100644 index 00000000000..5704431f15c --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/MultiHttpBlockConfigTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.web.bind.annotation.GetMapping; + +import static org.assertj.core.api.Assertions.assertThatCode; +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.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests scenarios with multiple <http> elements. + * + * @author Luke Taylor + */ +public class MultiHttpBlockConfigTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/MultiHttpBlockConfigTests"; + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void requestWhenUsingMutuallyExclusiveHttpElementsThenIsRoutedAccordingly() + throws Exception { + + this.spring.configLocations(this.xml("DistinctHttpElements")).autowire(); + + this.mvc.perform(MockMvcRequestBuilders.get("/first") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(post("/second/login") + .param("username", "user") + .param("password", "password") + .with(csrf())) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/")); + } + + @Test + public void configureWhenUsingDuplicateHttpElementsThenThrowsWiringException() { + assertThatCode(() -> this.spring.configLocations(this.xml("IdenticalHttpElements")).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void configureWhenUsingIndenticallyPatternedHttpElementsThenThrowsWiringException() { + assertThatCode(() -> this.spring.configLocations(this.xml("IdenticallyPatternedHttpElements")).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class); + } + + /** + * SEC-1937 + */ + @Test + public void requestWhenTargettingAuthenticationManagersToCorrespondingHttpElementsThenAuthenticationProceeds() + throws Exception { + + this.spring.configLocations(this.xml("Sec1937")).autowire(); + + this.mvc.perform(get("/first") + .with(httpBasic("first", "password")) + .with(csrf())) + .andExpect(status().isOk()); + + this.mvc.perform(post("/second/login") + .param("username", "second") + .param("password", "password") + .with(csrf())) + .andExpect(redirectedUrl("/")); + } + + @Controller + static class BasicController { + @GetMapping("/first") + public String first() { + return "ok"; + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/NamespaceHttpBasicTests.java b/config/src/test/java/org/springframework/security/config/http/NamespaceHttpBasicTests.java index da546e69e1d..449320ba5cb 100644 --- a/config/src/test/java/org/springframework/security/config/http/NamespaceHttpBasicTests.java +++ b/config/src/test/java/org/springframework/security/config/http/NamespaceHttpBasicTests.java @@ -52,7 +52,7 @@ public class NamespaceHttpBasicTests { @Before public void setup() { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setMethod("GET"); this.response = new MockHttpServletResponse(); this.chain = new MockFilterChain(); diff --git a/config/src/test/java/org/springframework/security/config/http/OpenIDConfigTests.java b/config/src/test/java/org/springframework/security/config/http/OpenIDConfigTests.java new file mode 100644 index 00000000000..07e81a2caf4 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/OpenIDConfigTests.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.HashSet; +import java.util.Set; +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Rule; +import org.junit.Test; +import org.openid4java.consumer.ConsumerManager; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.openid.OpenID4JavaConsumer; +import org.springframework.security.openid.OpenIDAuthenticationFilter; +import org.springframework.security.openid.OpenIDConsumer; +import org.springframework.security.util.FieldUtils; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.openid4java.discovery.yadis.YadisResolver.YADIS_XRDS_LOCATION; +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.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests usage of the <openid-login> element + * + * @author Luke Taylor + */ +public class OpenIDConfigTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/OpenIDConfigTests"; + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void requestWhenOpenIDAndFormLoginBothConfiguredThenRedirectsToGeneratedLoginPage() + throws Exception { + + this.spring.configLocations(this.xml("WithFormLogin")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + + assertThat(getFilter(DefaultLoginPageGeneratingFilter.class)).isNotNull(); + } + + @Test + public void requestWhenOpenIDAndFormLoginWithFormLoginPageConfiguredThenFormLoginPageWins() + throws Exception { + + this.spring.configLocations(this.xml("WithFormLoginPage")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/form-page")); + } + + @Test + public void requestWhenOpenIDAndFormLoginWithOpenIDLoginPageConfiguredThenOpenIDLoginPageWins() + throws Exception { + + this.spring.configLocations(this.xml("WithOpenIDLoginPageAndFormLogin")).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/openid-page")); + } + + @Test + public void configureWhenOpenIDAndFormLoginBothConfigureLoginPagesThenWiringException() + throws Exception { + + assertThatCode(() -> this.spring.configLocations(this.xml("WithFormLoginAndOpenIDLoginPages")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @Test + public void requestWhenOpenIDAndRememberMeConfiguredThenRememberMePassedToIdp() + throws Exception { + + this.spring.configLocations(this.xml("WithRememberMe")).autowire(); + + OpenIDAuthenticationFilter openIDFilter = getFilter(OpenIDAuthenticationFilter.class); + + String openIdEndpointUrl = "http://testopenid.com?openid.return_to="; + Set returnToUrlParameters = new HashSet<>(); + returnToUrlParameters.add(AbstractRememberMeServices.DEFAULT_PARAMETER); + openIDFilter.setReturnToUrlParameters(returnToUrlParameters); + + OpenIDConsumer consumer = mock(OpenIDConsumer.class); + when(consumer.beginConsumption(any(HttpServletRequest.class), anyString(), anyString(), anyString())) + .then(invocation -> openIdEndpointUrl + invocation.getArgument(2)); + openIDFilter.setConsumer(consumer); + + String expectedReturnTo = new StringBuilder("http://localhost/login/openid").append("?") + .append(AbstractRememberMeServices.DEFAULT_PARAMETER) + .append("=").append("on").toString(); + + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + + this.mvc.perform(get("/login")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString(AbstractRememberMeServices.DEFAULT_PARAMETER))); + + this.mvc.perform(get("/login/openid") + .param(OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD, "http://hey.openid.com/") + .param(AbstractRememberMeServices.DEFAULT_PARAMETER, "on")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl(openIdEndpointUrl + expectedReturnTo)); + } + + @Test + public void requestWhenAttributeExchangeConfiguredThenFetchAttributesPassedToIdp() + throws Exception { + + this.spring.configLocations(this.xml("WithOpenIDAttributes")).autowire(); + + OpenIDAuthenticationFilter openIDFilter = getFilter(OpenIDAuthenticationFilter.class); + OpenID4JavaConsumer consumer = getFieldValue(openIDFilter, "consumer"); + ConsumerManager manager = getFieldValue(consumer, "consumerManager"); + manager.setMaxAssocAttempts(0); + + try ( MockWebServer server = new MockWebServer() ) { + String endpoint = server.url("/").toString(); + + server.enqueue(new MockResponse() + .addHeader(YADIS_XRDS_LOCATION, endpoint)); + server.enqueue(new MockResponse() + .setBody(String.format( + "%s", + endpoint))); + + this.mvc.perform(get("/login/openid") + .param(OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD, endpoint)) + .andExpect(status().isFound()) + .andExpect(result -> result.getResponse().getRedirectedUrl().endsWith( + "openid.ext1.type.nickname=http%3A%2F%2Fschema.openid.net%2FnamePerson%2Ffriendly&" + + "openid.ext1.if_available=nickname&" + + "openid.ext1.type.email=http%3A%2F%2Fschema.openid.net%2Fcontact%2Femail&" + + "openid.ext1.required=email&" + + "openid.ext1.count.email=2")); + } + } + + /** + * SEC-2919 + */ + @Test + public void requestWhenLoginPageConfiguredWithPhraseLoginThenRedirectsOnlyToUserGeneratedLoginPage() + throws Exception { + + this.spring.configLocations(this.xml("Sec2919")).autowire(); + + assertThat(getFilter(DefaultLoginPageGeneratingFilter.class)).isNull(); + + this.mvc.perform(get("/login")) + .andExpect(status().isOk()) + .andExpect(content().string("a custom login page")); + } + + @RestController + static class CustomLoginController { + @GetMapping("/login") + public String custom() { + return "a custom login page"; + } + } + + private T getFilter(Class clazz) { + FilterChainProxy filterChain = this.spring.getContext().getBean(FilterChainProxy.class); + return (T) filterChain.getFilters("/").stream() + .filter(clazz::isInstance) + .findFirst() + .orElse(null); + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } + + private static T getFieldValue(Object bean, String fieldName) throws IllegalAccessException { + return (T) FieldUtils.getFieldValue(bean, fieldName); + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java b/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java new file mode 100644 index 00000000000..070cf441b7f --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/PlaceHolderAndELConfigTests.java @@ -0,0 +1,212 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +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.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Josh Cummings + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SecurityTestExecutionListeners +public class PlaceHolderAndELConfigTests { + + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/PlaceHolderAndELConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void getWhenUsingPlaceholderThenUnsecuredPatternCorrectlyConfigured() + throws Exception { + + System.setProperty("pattern.nofilters", "/unsecured"); + + this.spring.configLocations(this.xml("UnsecuredPattern")).autowire(); + + this.mvc.perform(get("/unsecured")) + .andExpect(status().isOk()); + } + + /** + * SEC-1201 + */ + @Test + public void loginWhenUsingPlaceholderThenInterceptUrlsAndFormLoginWorks() + throws Exception { + + System.setProperty("secure.Url", "/secured"); + System.setProperty("secure.role", "ROLE_NUNYA"); + System.setProperty("login.page", "/loginPage"); + System.setProperty("default.target", "/defaultTarget"); + System.setProperty("auth.failure", "/authFailure"); + + this.spring.configLocations(this.xml("InterceptUrlAndFormLogin")).autowire(); + + // login-page setting + + this.mvc.perform(get("/secured")) + .andExpect(redirectedUrl("http://localhost/loginPage")); + + // login-processing-url setting + // default-target-url setting + + this.mvc.perform(post("/loginPage") + .param("username", "user") + .param("password", "password")) + .andExpect(redirectedUrl("/defaultTarget")); + + // authentication-failure-url setting + + this.mvc.perform(post("/loginPage") + .param("username", "user") + .param("password", "wrong")) + .andExpect(redirectedUrl("/authFailure")); + } + + /** + * SEC-1309 + */ + @Test + public void loginWhenUsingSpELThenInterceptUrlsAndFormLoginWorks() + throws Exception { + + System.setProperty("secure.url", "/secured"); + System.setProperty("secure.role", "ROLE_NUNYA"); + System.setProperty("login.page", "/loginPage"); + System.setProperty("default.target", "/defaultTarget"); + System.setProperty("auth.failure", "/authFailure"); + + this.spring.configLocations( + this.xml("InterceptUrlAndFormLoginWithSpEL")).autowire(); + + // login-page setting + + this.mvc.perform(get("/secured")) + .andExpect(redirectedUrl("http://localhost/loginPage")); + + // login-processing-url setting + // default-target-url setting + + this.mvc.perform(post("/loginPage") + .param("username", "user") + .param("password", "password")) + .andExpect(redirectedUrl("/defaultTarget")); + + // authentication-failure-url setting + + this.mvc.perform(post("/loginPage") + .param("username", "user") + .param("password", "wrong")) + .andExpect(redirectedUrl("/authFailure")); + + } + + @Test + @WithMockUser + public void requestWhenUsingPlaceholderOrSpELThenPortMapperWorks() + throws Exception { + + System.setProperty("http", "9080"); + System.setProperty("https", "9443"); + + this.spring.configLocations(this.xml("PortMapping")).autowire(); + + this.mvc.perform(get("http://localhost:9080/secured")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("https://localhost:9443/secured")); + + this.mvc.perform(get("https://localhost:9443/unsecured")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost:9080/unsecured")); + } + + @Test + @WithMockUser + public void requestWhenUsingPlaceholderThenRequiresChannelWorks() + throws Exception { + + System.setProperty("secure.url", "/secured"); + System.setProperty("required.channel", "https"); + + this.spring.configLocations(this.xml("RequiresChannel")).autowire(); + + this.mvc.perform(get("http://localhost/secured")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("https://localhost/secured")); + } + + @Test + @WithMockUser + public void requestWhenUsingPlaceholderThenAccessDeniedPageWorks() + throws Exception { + + System.setProperty("accessDenied", "/go-away"); + + this.spring.configLocations(this.xml("AccessDeniedPage")).autowire(); + + this.mvc.perform(get("/secured")) + .andExpect(forwardedUrl("/go-away")); + } + + @Test + @WithMockUser + public void requestWhenUsingSpELThenAccessDeniedPageWorks() + throws Exception { + + this.spring.configLocations(this.xml("AccessDeniedPageWithSpEL")).autowire(); + + this.mvc.perform(get("/secured")) + .andExpect(forwardedUrl("/go-away")); + } + + @RestController + static class SimpleController { + @GetMapping("/unsecured") + String unsecured() { + return "unsecured"; + } + + @GetMapping("/secured") + String secured() { + return "secured"; + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/RememberMeConfigTests.java b/config/src/test/java/org/springframework/security/config/http/RememberMeConfigTests.java new file mode 100644 index 00000000000..b1983250ab7 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/RememberMeConfigTests.java @@ -0,0 +1,378 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.Collections; +import javax.servlet.http.Cookie; + +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.TestDataSource; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.DEFAULT_PARAMETER; +import static org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY; +import static org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl.CREATE_TABLE_SQL; +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.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Luke Taylor + * @author Rob Winch + * @author Oliver Becker + */ +public class RememberMeConfigTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/RememberMeConfigTests"; + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void requestWithRememberMeWhenUsingCustomTokenRepositoryThenAutomaticallyReauthenticates() + throws Exception { + + this.spring.configLocations(this.xml("WithTokenRepository")).autowire(); + + MvcResult result = this.rememberAuthentication("user", "password") + .andExpect(cookie().secure(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, false)) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + + JdbcTemplate template = this.spring.getContext().getBean(JdbcTemplate.class); + int count = template.queryForObject("select count(*) from persistent_logins", int.class); + assertThat(count).isEqualTo(1); + } + + @Test + public void requestWithRememberMeWhenUsingCustomDataSourceThenAutomaticallyReauthenticates() + throws Exception { + + this.spring.configLocations(this.xml("WithDataSource")).autowire(); + + TestDataSource dataSource = this.spring.getContext().getBean(TestDataSource.class); + JdbcTemplate template = new JdbcTemplate(dataSource); + template.execute(CREATE_TABLE_SQL); + + MvcResult result = this.rememberAuthentication("user", "password") + .andExpect(cookie().secure(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, false)) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + + int count = template.queryForObject("select count(*) from persistent_logins", int.class); + assertThat(count).isEqualTo(1); + } + + @Test + public void requestWithRememberMeWhenUsingAuthenticationSuccessHandlerThenInvokesHandler() + throws Exception { + + this.spring.configLocations(this.xml("WithAuthenticationSuccessHandler")).autowire(); + + TestDataSource dataSource = this.spring.getContext().getBean(TestDataSource.class); + JdbcTemplate template = new JdbcTemplate(dataSource); + template.execute(CREATE_TABLE_SQL); + + MvcResult result = this.rememberAuthentication("user", "password") + .andExpect(cookie().secure(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, false)) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(redirectedUrl("/target")); + + int count = template.queryForObject("select count(*) from persistent_logins", int.class); + assertThat(count).isEqualTo(1); + } + + @Test + public void requestWithRememberMeWhenUsingCustomRememberMeServicesThenAuthenticates() + throws Exception { + // SEC-1281 - using key with external services + this.spring.configLocations(this.xml("WithServicesRef")).autowire(); + + MvcResult result = this.rememberAuthentication("user", "password") + .andExpect(cookie().secure(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, false)) + .andExpect(cookie().maxAge(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, 5000)) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + + // SEC-909 + this.mvc.perform(post("/logout") + .cookie(cookie) + .with(csrf())) + .andExpect(cookie().maxAge(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, 0)) + .andReturn(); + } + + @Test + public void logoutWhenUsingRememberMeDefaultsThenCookieIsCancelled() + throws Exception { + + this.spring.configLocations(this.xml("DefaultConfig")).autowire(); + + MvcResult result = this.rememberAuthentication("user", "password").andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(post("/logout") + .cookie(cookie) + .with(csrf())) + .andExpect(cookie().maxAge(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, 0)); + } + + @Test + public void requestWithRememberMeWhenTokenValidityIsConfiguredThenCookieReflectsCorrectExpiration() + throws Exception { + + this.spring.configLocations(this.xml("TokenValidity")).autowire(); + + MvcResult result = this.rememberAuthentication("user", "password") + .andExpect(cookie().maxAge(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, 10000)) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + } + + @Test + public void requestWithRememberMeWhenTokenValidityIsNegativeThenCookieReflectsCorrectExpiration() + throws Exception { + + this.spring.configLocations(this.xml("NegativeTokenValidity")).autowire(); + + this.rememberAuthentication("user", "password") + .andExpect(cookie().maxAge(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, -1)); + } + + + @Test + public void configureWhenUsingDataSourceAndANegativeTokenValidityThenThrowsWiringException() { + assertThatCode(() -> this.spring.configLocations(this.xml("NegativeTokenValidityWithDataSource")).autowire()) + .isInstanceOf(FatalBeanException.class); + } + + @Test + public void requestWithRememberMeWhenTokenValidityIsResolvedByPropertyPlaceholderThenCookieReflectsCorrectExpiration() + throws Exception { + + this.spring.configLocations(this.xml("Sec2165")).autowire(); + + this.rememberAuthentication("user", "password") + .andExpect(cookie().maxAge(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, 30)); + } + + @Test + public void requestWithRememberMeWhenUseSecureCookieIsTrueThenCookieIsSecure() + throws Exception { + + this.spring.configLocations(this.xml("SecureCookie")).autowire(); + + this.rememberAuthentication("user", "password") + .andExpect(cookie().secure(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, true)); + } + + /** + * SEC-1827 + */ + @Test + public void requestWithRememberMeWhenUseSecureCookieIsFalseThenCookieIsNotSecure() + throws Exception { + + this.spring.configLocations(this.xml("Sec1827")).autowire(); + + this.rememberAuthentication("user", "password") + .andExpect(cookie().secure(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, false)); + } + + @Test + public void configureWhenUsingPersistentTokenRepositoryAndANegativeTokenValidityThenThrowsWiringException() { + assertThatCode(() -> this.spring.configLocations(this.xml("NegativeTokenValidityWithPersistentRepository")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + @Test + public void requestWithRememberMeWhenUsingCustomUserDetailsServiceThenInvokesThisUserDetailsService() + throws Exception { + this.spring.configLocations(this.xml("WithUserDetailsService")).autowire(); + + UserDetailsService userDetailsService = this.spring.getContext().getBean(UserDetailsService.class); + when(userDetailsService.loadUserByUsername("user")).thenAnswer((invocation) -> + new User("user", "{noop}password", Collections.emptyList())); + + MvcResult result = this.rememberAuthentication("user", "password").andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + + verify(userDetailsService, atLeastOnce()).loadUserByUsername("user"); + } + + /** + * SEC-742 + */ + @Test + public void requestWithRememberMeWhenExcludingBasicAuthenticationFilterThenStillReauthenticates() + throws Exception { + + this.spring.configLocations(this.xml("Sec742")).autowire(); + + MvcResult result = + this.mvc.perform(login("user", "password") + .param("remember-me", "true") + .with(csrf())) + .andExpect(redirectedUrl("/messageList.html")) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + } + + /** + * SEC-2119 + */ + @Test + public void requestWithRememberMeWhenUsingCustomRememberMeParameterThenReauthenticates() + throws Exception { + + this.spring.configLocations(this.xml("WithRememberMeParameter")).autowire(); + + MvcResult result = + this.mvc.perform(login("user", "password") + .param("custom-remember-me-parameter", "true") + .with(csrf())) + .andExpect(redirectedUrl("/")) + .andReturn(); + + Cookie cookie = rememberMeCookie(result); + + this.mvc.perform(get("/authenticated") + .cookie(cookie)) + .andExpect(status().isOk()); + } + + @Test + public void configureWhenUsingRememberMeParameterAndServicesRefThenThrowsWiringException() { + assertThatCode(() -> this.spring.configLocations(this.xml("WithRememberMeParameterAndServicesRef")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class); + } + + /** + * SEC-2826 + */ + @Test + public void authenticateWhenUsingCustomRememberMeCookieNameThenIssuesCookieWithThatName() + throws Exception { + + this.spring.configLocations(this.xml("WithRememberMeCookie")).autowire(); + + this.rememberAuthentication("user", "password") + .andExpect(cookie().exists("custom-remember-me-cookie")); + } + + /** + * SEC-2826 + */ + @Test + public void configureWhenUsingRememberMeCookieAndServicesRefThenThrowsWiringException() { + assertThatCode(() -> this.spring.configLocations(this.xml("WithRememberMeCookieAndServicesRef")).autowire()) + .isInstanceOf(BeanDefinitionParsingException.class) + .hasMessageContaining("Configuration problem: services-ref can't be used in combination with attributes " + + "token-repository-ref,data-source-ref, user-service-ref, token-validity-seconds, use-secure-cookie, " + + "remember-me-parameter or remember-me-cookie"); + } + + @RestController + static class BasicController { + @GetMapping("/authenticated") + String ok() { + return "ok"; + } + } + + private ResultActions rememberAuthentication(String username, String password) + throws Exception { + + return this.mvc.perform(login(username, password) + .param(DEFAULT_PARAMETER, "true") + .with(csrf())) + .andExpect(redirectedUrl("/")); + } + + private static MockHttpServletRequestBuilder login(String username, String password) { + return post("/login").param("username", username).param("password", password); + } + + private static Cookie rememberMeCookie(MvcResult result) { + return result.getResponse().getCookie("remember-me"); + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java b/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java new file mode 100644 index 00000000000..af22e07fc4d --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests.java @@ -0,0 +1,302 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 org.apache.http.HttpHeaders; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.core.StringContains.containsString; +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.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Rob Winch + * @author Josh Cummings + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SecurityTestExecutionListeners +public class SecurityContextHolderAwareRequestConfigTests { + + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private MockMvc mvc; + + @Test + public void servletLoginWhenUsingDefaultConfigurationThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("Simple")).autowire(); + + this.mvc.perform(get("/good-login")) + .andExpect(status().isOk()) + .andExpect(content().string("user")); + } + + @Test + public void servletAuthenticateWhenUsingDefaultConfigurationThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("Simple")).autowire(); + + this.mvc.perform(get("/authenticate")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + public void servletLogoutWhenUsingDefaultConfigurationThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("Simple")).autowire(); + + MvcResult result = this.mvc.perform(get("/good-login")).andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNotNull(); + + result = this.mvc.perform(get("/do-logout").session(session)) + .andExpect(status().isOk()) + .andExpect(content().string("")) + .andReturn(); + + session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNull(); + } + + @Test + public void servletAuthenticateWhenUsingHttpBasicThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("HttpBasic")).autowire(); + + this.mvc.perform(get("/authenticate")) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("discworld"))); + } + + @Test + public void servletAuthenticateWhenUsingFormLoginThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("FormLogin")).autowire(); + + this.mvc.perform(get("/authenticate")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + public void servletLoginWhenUsingMultipleHttpConfigsThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("MultiHttp")).autowire(); + + this.mvc.perform(get("/good-login")) + .andExpect(status().isOk()) + .andExpect(content().string("user")); + + this.mvc.perform(get("/v2/good-login")) + .andExpect(status().isOk()) + .andExpect(content().string("user2")); + } + + @Test + public void servletAuthenticateWhenUsingMultipleHttpConfigsThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("MultiHttp")).autowire(); + + this.mvc.perform(get("/authenticate")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + + this.mvc.perform(get("/v2/authenticate")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login2")); + + } + + @Test + public void servletLogoutWhenUsingMultipleHttpConfigsThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("MultiHttp")).autowire(); + + MvcResult result = this.mvc.perform(get("/good-login")).andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNotNull(); + + result = this.mvc.perform(get("/do-logout").session(session)) + .andExpect(status().isOk()) + .andExpect(content().string("")) + .andReturn(); + + session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNotNull(); + + result = this.mvc.perform(get("/v2/good-login")).andReturn(); + + session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNotNull(); + + result = this.mvc.perform(get("/v2/do-logout").session(session)) + .andExpect(status().isOk()) + .andExpect(content().string("")) + .andReturn(); + + session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNull(); + } + + @Test + public void servletLogoutWhenUsingCustomLogoutThenUsesSpringSecurity() + throws Exception { + + this.spring.configLocations(this.xml("Logout")).autowire(); + + this.mvc.perform(get("/authenticate")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/signin")); + + MvcResult result = this.mvc.perform(get("/good-login")).andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNotNull(); + + result = this.mvc.perform(get("/do-logout").session(session)) + .andExpect(status().isOk()) + .andExpect(content().string("")) + .andExpect(cookie().maxAge("JSESSIONID", 0)) + .andReturn(); + + session = (MockHttpSession) result.getRequest().getSession(false); + + assertThat(session).isNotNull(); + } + + /** + * SEC-2926: Role Prefix is set + */ + @Test + @WithMockUser + public void servletIsUserInRoleWhenUsingDefaultConfigThenRoleIsSet() + throws Exception { + + this.spring.configLocations(this.xml("Simple")).autowire(); + + this.mvc.perform(get("/role")).andExpect(content().string("true")); + } + + @RestController + public static class ServletAuthenticatedController { + @GetMapping("/v2/good-login") + public String v2Login(HttpServletRequest request) throws ServletException { + + request.login("user2", "password2"); + + return this.principal(); + } + + @GetMapping("/good-login") + public String login(HttpServletRequest request) throws ServletException { + + request.login("user", "password"); + + return this.principal(); + } + + @GetMapping("/v2/authenticate") + public String v2Authenticate(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + + return this.authenticate(request, response); + } + + @GetMapping("/authenticate") + public String authenticate(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + + request.authenticate(response); + + return this.principal(); + } + + @GetMapping("/v2/do-logout") + public String v2Logout(HttpServletRequest request) throws ServletException { + return this.logout(request); + } + + @GetMapping("/do-logout") + public String logout(HttpServletRequest request) throws ServletException { + request.logout(); + + return this.principal(); + } + + @GetMapping("/role") + public String role(HttpServletRequest request) { + return String.valueOf(request.isUserInRole("USER")); + } + + private String principal() { + if ( SecurityContextHolder.getContext().getAuthentication() != null ) { + return SecurityContextHolder.getContext().getAuthentication().getName(); + } + return null; + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigServlet31Tests.java b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigServlet31Tests.java index 19da353fbd7..a1bf8cea93b 100644 --- a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigServlet31Tests.java +++ b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigServlet31Tests.java @@ -73,7 +73,7 @@ public class SessionManagementConfigServlet31Tests { @Before public void setup() { - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); response = new MockHttpServletResponse(); chain = new MockFilterChain(); } @@ -89,7 +89,7 @@ public void teardown() { public void changeSessionIdDefaultsInServlet31Plus() throws Exception { spy(ReflectionUtils.class); Method method = mock(Method.class); - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.getSession(); request.setServletPath("/login"); request.setMethod("POST"); @@ -112,7 +112,7 @@ public void changeSessionIdDefaultsInServlet31Plus() throws Exception { public void changeSessionId() throws Exception { spy(ReflectionUtils.class); Method method = mock(Method.class); - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.getSession(); request.setServletPath("/login"); request.setMethod("POST"); diff --git a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTests.java b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTests.java new file mode 100644 index 00000000000..0ff6d2edf1f --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTests.java @@ -0,0 +1,667 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.io.IOException; +import java.security.Principal; +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.apache.http.HttpStatus; +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.session.SessionRegistry; +import org.springframework.security.util.FieldUtils; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.session.SessionAuthenticationException; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.SessionManagementFilter; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +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.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; +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.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests session-related functionality for the <http> namespace element and <session-management> + * + * @author Luke Taylor + * @author Rob Winch + * @author Josh Cummings + */ +public class SessionManagementConfigTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/SessionManagementConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void requestWhenCreateSessionAlwaysThenAlwaysCreatesSession() + throws Exception { + this.spring.configLocations(this.xml("CreateSessionAlways")).autowire(); + + MockHttpServletRequest request = get("/").buildRequest(this.servletContext()); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + assertThat(request.getSession(false)).isNotNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToNeverThenDoesNotCreateSessionOnLoginChallenge() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionNever")).autowire(); + + MockHttpServletRequest request = get("/auth").buildRequest(this.servletContext()); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToNeverThenDoesNotCreateSessionOnLogin() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionNever")).autowire(); + + MockHttpServletRequest request = post("/login") + .param("username", "user") + .param("password", "password") + .buildRequest(this.servletContext()); + request = csrf().postProcessRequest(request); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToNeverThenUsesExistingSession() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionNever")).autowire(); + + MockHttpServletRequest request = post("/login") + .param("username", "user") + .param("password", "password") + .buildRequest(this.servletContext()); + request = csrf().postProcessRequest(request); + MockHttpSession session = new MockHttpSession(); + request.setSession(session); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNotNull(); + assertThat(request.getSession(false).getAttribute(SPRING_SECURITY_CONTEXT_KEY)) + .isNotNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToStatelessThenDoesNotCreateSessionOnLoginChallenge() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionStateless")).autowire(); + + this.mvc.perform(get("/auth")) + .andExpect(status().isFound()) + .andExpect(session().exists(false)); + } + + @Test + public void requestWhenCreateSessionIsSetToStatelessThenDoesNotCreateSessionOnLogin() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionStateless")).autowire(); + + + this.mvc.perform(post("/login") + .param("username", "user") + .param("password", "password") + .with(csrf())) + .andExpect(status().isFound()) + .andExpect(session().exists(false)); + } + + @Test + public void requestWhenCreateSessionIsSetToStatelessThenIgnoresExistingSession() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionStateless")).autowire(); + + MvcResult result = + this.mvc.perform(post("/login") + .param("username", "user") + .param("password", "password") + .session(new MockHttpSession()) + .with(csrf())) + .andExpect(status().isFound()) + .andExpect(session()) + .andReturn(); + + assertThat(result.getRequest().getSession(false).getAttribute(SPRING_SECURITY_CONTEXT_KEY)) + .isNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToIfRequiredThenDoesNotCreateSessionOnPublicInvocation() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionIfRequired")).autowire(); + + ServletContext servletContext = this.mvc.getDispatcherServlet().getServletContext(); + MockHttpServletRequest request = get("/").buildRequest(servletContext); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + assertThat(request.getSession(false)).isNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToIfRequiredThenCreatesSessionOnLoginChallenge() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionIfRequired")).autowire(); + + ServletContext servletContext = this.mvc.getDispatcherServlet().getServletContext(); + MockHttpServletRequest request = get("/auth").buildRequest(servletContext); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNotNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToIfRequiredThenCreatesSessionOnLogin() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionIfRequired")).autowire(); + + ServletContext servletContext = this.mvc.getDispatcherServlet().getServletContext(); + MockHttpServletRequest request = post("/login") + .param("username", "user") + .param("password", "password") + .buildRequest(servletContext); + request = csrf().postProcessRequest(request); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNotNull(); + } + + /** + * SEC-1208 + */ + @Test + public void requestWhenRejectingUserBasedOnMaxSessionsExceededThenDoesNotCreateSession() + throws Exception { + + this.spring.configLocations(this.xml("Sec1208")).autowire(); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(session()); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isUnauthorized()) + .andExpect(session().exists(false)); + } + + /** + * SEC-2137 + */ + @Test + public void requestWhenSessionFixationProtectionDisabledAndConcurrencyControlEnabledThenSessionNotInvalidated() + throws Exception { + + this.spring.configLocations(this.xml("Sec2137")).autowire(); + + MockHttpSession session = new MockHttpSession(); + this.mvc.perform(get("/auth") + .session(session) + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(session().id(session.getId())); + } + + @Test + public void autowireWhenExportingSessionRegistryBeanThenAvailableForWiring() { + this.spring.configLocations(this.xml("ConcurrencyControlSessionRegistryAlias")).autowire(); + + this.sessionRegistryIsValid(); + } + + @Test + public void requestWhenExpiredUrlIsSetThenInvalidatesSessionAndRedirects() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlExpiredUrl")).autowire(); + + this.mvc.perform(get("/auth") + .session(this.expiredSession()) + .with(httpBasic("user", "password"))) + .andExpect(redirectedUrl("/expired")) + .andExpect(session().exists(false)); + } + + @Test + public void requestWhenConcurrencyControlAndCustomLogoutHandlersAreSetThenAllAreInvokedWhenSessionExpires() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlLogoutAndRememberMeHandlers")).autowire(); + + this.mvc.perform(get("/auth") + .session(this.expiredSession()) + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(cookie().maxAge("testCookie", 0)) + .andExpect(cookie().exists("rememberMeCookie")) + .andExpect(session().valid(true)); + } + + @Test + public void requestWhenConcurrencyControlAndRememberMeAreSetThenInvokedWhenSessionExpires() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlRememberMeHandler")).autowire(); + + this.mvc.perform(get("/auth") + .session(this.expiredSession()) + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(cookie().exists("rememberMeCookie")) + .andExpect(session().exists(false)); + } + + /** + * SEC-2057 + */ + @Test + public void autowireWhenConcurrencyControlIsSetThenLogoutHandlersGetAuthenticationObject() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlCustomLogoutHandler")).autowire(); + + MvcResult result = + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(session()) + .andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + + SessionRegistry sessionRegistry = this.spring.getContext().getBean(SessionRegistry.class); + sessionRegistry.getSessionInformation(session.getId()).expireNow(); + + this.mvc.perform(get("/auth") + .session(session)) + .andExpect(header().string("X-Username", "user")); + } + + @Test + public void requestWhenConcurrencyControlIsSetThenDefaultsToResponseBodyExpirationResponse() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlSessionRegistryAlias")).autowire(); + + this.mvc.perform(get("/auth") + .session(this.expiredSession()) + .with(httpBasic("user", "password"))) + .andExpect(content().string("This session has been expired (possibly due to multiple concurrent " + + "logins being attempted as the same user).")); + } + + @Test + public void requestWhenCustomSessionAuthenticationStrategyThenInvokesOnAuthentication() + throws Exception { + + this.spring.configLocations(this.xml("SessionAuthenticationStrategyRef")).autowire(); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isIAmATeapot()); + } + + @Test + public void autowireWhenSessionRegistryRefIsSetThenAvailableForWiring() { + this.spring.configLocations(this.xml("ConcurrencyControlSessionRegistryRef")).autowire(); + + this.sessionRegistryIsValid(); + } + + @Test + public void requestWhenMaxSessionsIsSetThenErrorsWhenExceeded() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlMaxSessions")).autowire(); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(redirectedUrl("/max-exceeded")); + } + + @Test + public void autowireWhenSessionFixationProtectionIsNoneAndCsrfDisabledThenSessionManagementFilterIsNotWired() { + + this.spring.configLocations(this.xml("NoSessionManagementFilter")).autowire(); + + assertThat(this.getFilter(SessionManagementFilter.class)).isNull(); + } + + @Test + public void requestWhenSessionFixationProtectionIsNoneThenSessionNotInvalidated() + throws Exception { + + this.spring.configLocations(this.xml("SessionFixationProtectionNone")).autowire(); + + MockHttpSession session = new MockHttpSession(); + String sessionId = session.getId(); + + this.mvc.perform(get("/auth") + .session(session) + .with(httpBasic("user", "password"))) + .andExpect(session().id(sessionId)); + } + + @Test + public void requestWhenSessionFixationProtectionIsMigrateSessionThenSessionIsReplaced() + throws Exception { + + this.spring.configLocations(this.xml("SessionFixationProtectionMigrateSession")).autowire(); + + MockHttpSession session = new MockHttpSession(); + String sessionId = session.getId(); + + MvcResult result = + this.mvc.perform(get("/auth") + .session(session) + .with(httpBasic("user", "password"))) + .andExpect(session()) + .andReturn(); + + assertThat(result.getRequest().getSession(false).getId()).isNotEqualTo(sessionId); + } + + @Test + public void requestWhenSessionFixationProtectionIsNoneAndInvalidSessionUrlIsSetThenStillRedirectsOnInvalidSession() + throws Exception { + + this.spring.configLocations(this.xml("SessionFixationProtectionNoneWithInvalidSessionUrl")).autowire(); + + this.mvc.perform(get("/auth") + .with(request -> { + request.setRequestedSessionId("1"); + request.setRequestedSessionIdValid(false); + return request; + })) + .andExpect(redirectedUrl("/timeoutUrl")); + } + + static class TeapotSessionAuthenticationStrategy implements SessionAuthenticationStrategy { + + @Override + public void onAuthentication( + Authentication authentication, + HttpServletRequest request, + HttpServletResponse response) throws SessionAuthenticationException { + + response.setStatus(org.springframework.http.HttpStatus.I_AM_A_TEAPOT.value()); + } + } + + static class CustomRememberMeServices implements RememberMeServices, LogoutHandler { + @Override + public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { + return null; + } + + @Override + public void loginFail(HttpServletRequest request, HttpServletResponse response) { + + } + + @Override + public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { + + } + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + response.addHeader("X-Username", authentication.getName()); + } + } + + @RestController + static class BasicController { + @GetMapping("/") + public String ok() { + return "ok"; + } + + @GetMapping("/auth") + public String auth(Principal principal) { + return principal.getName(); + } + } + + private void sessionRegistryIsValid() { + SessionRegistry sessionRegistry = this.spring.getContext() + .getBean("sessionRegistry", SessionRegistry.class); + + assertThat(sessionRegistry).isNotNull(); + + assertThat(this.getFilter(ConcurrentSessionFilter.class)) + .returns(sessionRegistry, this::extractSessionRegistry); + assertThat(this.getFilter(UsernamePasswordAuthenticationFilter.class)) + .returns(sessionRegistry, this::extractSessionRegistry); + // SEC-1143 + assertThat(this.getFilter(SessionManagementFilter.class)) + .returns(sessionRegistry, this::extractSessionRegistry); + } + + private SessionRegistry extractSessionRegistry(ConcurrentSessionFilter filter) { + return getFieldValue(filter, "sessionRegistry"); + } + + private SessionRegistry extractSessionRegistry(UsernamePasswordAuthenticationFilter filter) { + SessionAuthenticationStrategy strategy = getFieldValue(filter, "sessionStrategy"); + List strategies = getFieldValue(strategy, "delegateStrategies"); + return getFieldValue(strategies.get(0), "sessionRegistry"); + } + + private SessionRegistry extractSessionRegistry(SessionManagementFilter filter) { + SessionAuthenticationStrategy strategy = getFieldValue(filter, "sessionAuthenticationStrategy"); + List strategies = getFieldValue(strategy, "delegateStrategies"); + return getFieldValue(strategies.get(0), "sessionRegistry"); + } + + private T getFieldValue(Object target, String fieldName) { + try { + return (T) FieldUtils.getFieldValue(target, fieldName); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static SessionResultMatcher session() { + return new SessionResultMatcher(); + } + + private static class SessionResultMatcher implements ResultMatcher { + private String id; + private Boolean valid; + private Boolean exists = true; + + public ResultMatcher exists(boolean exists) { + this.exists = exists; + return this; + } + + public ResultMatcher valid(boolean valid) { + this.valid = valid; + return this.exists(true); + } + + public ResultMatcher id(String id) { + this.id = id; + return this.exists(true); + } + + @Override + public void match(MvcResult result) { + if (!this.exists) { + assertThat(result.getRequest().getSession(false)).isNull(); + return; + } + + assertThat(result.getRequest().getSession(false)).isNotNull(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + + if (this.valid != null) { + if (this.valid) { + assertThat(session.isInvalid()).isFalse(); + } else { + assertThat(session.isInvalid()).isTrue(); + } + } + + if (this.id != null) { + assertThat(session.getId()).isEqualTo(this.id); + } + } + } + + private static MockHttpServletResponse request( + MockHttpServletRequest request, + ApplicationContext context) + throws IOException, ServletException { + + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChainProxy proxy = context.getBean(FilterChainProxy.class); + + proxy.doFilter( + request, + new EncodeUrlDenyingHttpServletResponseWrapper(response), + (req, resp) -> {}); + + return response; + } + + private static class EncodeUrlDenyingHttpServletResponseWrapper + extends HttpServletResponseWrapper { + + public EncodeUrlDenyingHttpServletResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public String encodeURL(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeRedirectURL(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeUrl(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeRedirectUrl(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + } + + private MockHttpSession expiredSession() { + MockHttpSession session = new MockHttpSession(); + SessionRegistry sessionRegistry = this.spring.getContext().getBean(SessionRegistry.class); + sessionRegistry.registerNewSession(session.getId(), "user"); + sessionRegistry.getSessionInformation(session.getId()).expireNow(); + return session; + } + + private T getFilter(Class filterClass) { + return (T) getFilters().stream() + .filter(filterClass::isInstance) + .findFirst() + .orElse(null); + } + + private List getFilters() { + FilterChainProxy proxy = this.spring.getContext().getBean(FilterChainProxy.class); + + return proxy.getFilters("/"); + } + + private ServletContext servletContext() { + WebApplicationContext context = (WebApplicationContext) this.spring.getContext(); + return context.getServletContext(); + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java new file mode 100644 index 00000000000..17d19f274c7 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.Transient; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +/** + * @author Josh Cummings + */ +public class SessionManagementConfigTransientAuthenticationTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests"; + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void postWhenTransientAuthenticationThenNoSessionCreated() + throws Exception { + + this.spring.configLocations(this.xml("WithTransientAuthentication")).autowire(); + MvcResult result = this.mvc.perform(post("/login")).andReturn(); + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @Test + public void postWhenTransientAuthenticationThenAlwaysSessionOverrides() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionAlwaysWithTransientAuthentication")).autowire(); + MvcResult result = this.mvc.perform(post("/login")).andReturn(); + assertThat(result.getRequest().getSession(false)).isNotNull(); + } + + static class TransientAuthenticationProvider implements AuthenticationProvider { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + return new SomeTransientAuthentication(); + } + + @Override + public boolean supports(Class authentication) { + return true; + } + } + + @Transient + static class SomeTransientAuthentication extends AbstractAuthenticationToken { + SomeTransientAuthentication() { + super(null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/customconfigurer/CustomHttpSecurityConfigurerTests.java b/config/src/test/java/org/springframework/security/config/http/customconfigurer/CustomHttpSecurityConfigurerTests.java index beda71be972..3d0bfa7edfb 100644 --- a/config/src/test/java/org/springframework/security/config/http/customconfigurer/CustomHttpSecurityConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/http/customconfigurer/CustomHttpSecurityConfigurerTests.java @@ -55,7 +55,7 @@ public class CustomHttpSecurityConfigurerTests { @Before public void setup() { - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); response = new MockHttpServletResponse(); chain = new MockFilterChain(); request.setMethod("GET"); diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java index 2e093fe94cd..6ff20bcac31 100644 --- a/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java @@ -89,7 +89,7 @@ public void getBuilderWhenFacebookShouldHaveFacebookSettings() throws Exception assertThat(providerDetails.getTokenUri()) .isEqualTo("https://graph.facebook.com/v2.8/oauth/access_token"); assertThat(providerDetails.getUserInfoEndpoint().getUri()) - .isEqualTo("https://graph.facebook.com/me"); + .isEqualTo("https://graph.facebook.com/me?fields=id,name,email"); assertThat(providerDetails.getUserInfoEndpoint().getUserNameAttributeName()) .isEqualTo("id"); assertThat(providerDetails.getJwkSetUri()).isNull(); diff --git a/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java new file mode 100644 index 00000000000..d777d60bc13 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java @@ -0,0 +1,117 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://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.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpHeaders; +import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; +import org.springframework.test.web.reactive.server.FluxExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsConfigurationSource; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class CorsSpecTests { + @Mock + private CorsConfigurationSource source; + @Mock + private ApplicationContext context; + + ServerHttpSecurity http; + + HttpHeaders expectedHeaders = new HttpHeaders(); + + Set headerNamesNotPresent = new HashSet<>(); + + @Before + public void setup() { + this.http = new TestingServerHttpSecurity() + .applicationContext(this.context); + CorsConfiguration value = new CorsConfiguration(); + value.setAllowedOrigins(Arrays.asList("*")); + when(this.source.getCorsConfiguration(any())).thenReturn(value); + } + + @Test + public void corsWhenEnabledThenAccessControlAllowOriginAndSecurityHeaders() { + this.http.cors().configurationSource(this.source); + this.expectedHeaders.set("Access-Control-Allow-Origin", "*"); + this.expectedHeaders.set("X-Frame-Options", "DENY"); + assertHeaders(); + } + + @Test + public void corsWhenCorsConfigurationSourceBeanThenAccessControlAllowOriginAndSecurityHeaders() { + when(this.context.getBeanNamesForType(any(ResolvableType.class))).thenReturn(new String[] {"source"}, new String[0]); + when(this.context.getBean("source")).thenReturn(this.source); + this.expectedHeaders.set("Access-Control-Allow-Origin", "*"); + this.expectedHeaders.set("X-Frame-Options", "DENY"); + assertHeaders(); + } + + @Test + public void corsWhenNoConfigurationSourceThenNoCorsHeaders() { + when(this.context.getBeanNamesForType(any(ResolvableType.class))).thenReturn(new String[0]); + this.headerNamesNotPresent.add("Access-Control-Allow-Origin"); + assertHeaders(); + } + + private void assertHeaders() { + WebTestClient client = buildClient(); + FluxExchangeResult response = client.get() + .uri("https://example.com/") + .headers(h -> h.setOrigin("https://origin.example.com")) + .exchange() + .returnResult(String.class); + + Map> responseHeaders = response.getResponseHeaders(); + + if (!this.expectedHeaders.isEmpty()) { + assertThat(responseHeaders).describedAs(response.toString()) + .containsAllEntriesOf(this.expectedHeaders); + } + if (!this.headerNamesNotPresent.isEmpty()) { + assertThat(responseHeaders.keySet()).doesNotContainAnyElementsOf(this.headerNamesNotPresent); + } + } + + private WebTestClient buildClient() { + return WebTestClientBuilder + .bindToWebFilters(this.http.build()) + .build(); + } +} diff --git a/config/src/test/java/org/springframework/security/config/web/server/ExceptionHandlingSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/ExceptionHandlingSpecTests.java index e88ab4848a0..d18e30fd464 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/ExceptionHandlingSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/ExceptionHandlingSpecTests.java @@ -25,8 +25,6 @@ import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler; import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; import org.springframework.test.web.reactive.server.WebTestClient; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; /** * @author Denys Ivano @@ -96,13 +94,12 @@ public void defaultAccessDeniedHandler() { WebTestClient client = WebTestClientBuilder .bindToWebFilters(securityWebFilter) - .filter(basicAuthentication()) .build(); client .get() .uri("/admin") - .attributes(basicAuthenticationCredentials("user", "password")) + .headers(headers -> headers.setBasicAuth("user", "password")) .exchange() .expectStatus().isForbidden(); } @@ -122,13 +119,12 @@ public void customAccessDeniedHandler() { WebTestClient client = WebTestClientBuilder .bindToWebFilters(securityWebFilter) - .filter(basicAuthentication()) .build(); client .get() .uri("/admin") - .attributes(basicAuthenticationCredentials("user", "password")) + .headers(headers -> headers.setBasicAuth("user", "password")) .exchange() .expectStatus().isBadRequest(); } diff --git a/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java index 70b0fa60f85..501c9fa22b2 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java @@ -17,6 +17,8 @@ package org.springframework.security.config.web.server; import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -36,6 +38,8 @@ import reactor.core.publisher.Mono; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Rob Winch @@ -204,9 +208,10 @@ public static class DefaultLoginPage { private LoginForm loginForm; + private OAuth2Login oauth2Login = new OAuth2Login(); + public DefaultLoginPage(WebDriver webDriver) { this.driver = webDriver; - this.loginForm = PageFactory.initElements(webDriver, LoginForm.class); } static DefaultLoginPage create(WebDriver driver) { @@ -228,10 +233,23 @@ public DefaultLoginPage assertLogout() { return this; } + public DefaultLoginPage assertLoginFormNotPresent() { + assertThatThrownBy(() -> loginForm().username("")) + .isInstanceOf(NoSuchElementException.class); + return this; + } + public LoginForm loginForm() { + if (this.loginForm == null) { + this.loginForm = PageFactory.initElements(this.driver, LoginForm.class); + } return this.loginForm; } + public OAuth2Login oauth2Login() { + return this.oauth2Login; + } + static DefaultLoginPage to(WebDriver driver) { driver.get("http://localhost/login"); return PageFactory.initElements(driver, DefaultLoginPage.class); @@ -263,6 +281,22 @@ public T submit(Class page) { return PageFactory.initElements(this.driver, page); } } + + public class OAuth2Login { + public WebElement findClientRegistrationByName(String clientName) { + return DefaultLoginPage.this.driver.findElement(By.linkText(clientName)); + } + + public OAuth2Login assertClientRegistrationByName(String clientName) { + assertThatCode(() -> findClientRegistrationByName(clientName)) + .doesNotThrowAnyException(); + return this; + } + + public DefaultLoginPage and() { + return DefaultLoginPage.this; + } + } } public static class DefaultLogoutPage { diff --git a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java index 562542477bb..e1b0dfe658c 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java @@ -110,8 +110,9 @@ public void headersWhenHstsDisableThenHstsNotWritten() { public void headersWhenHstsCustomThenCustomHstsWritten() { this.expectedHeaders.remove(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY); this.expectedHeaders.add(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=60"); - this.headers.hsts().maxAge(Duration.ofSeconds(60)); - this.headers.hsts().includeSubdomains(false); + this.headers.hsts() + .maxAge(Duration.ofSeconds(60)) + .includeSubdomains(false); assertHeaders(); } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java new file mode 100644 index 00000000000..a2b4e1f73f4 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Rule; +import org.junit.Test; +import org.openqa.selenium.WebDriver; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; +import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; +import org.springframework.security.web.server.WebFilterChainProxy; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +import reactor.core.publisher.Mono; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class OAuth2LoginTests { + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private WebFilterChainProxy springSecurity; + + private static ClientRegistration github = CommonOAuth2Provider.GITHUB + .getBuilder("github") + .clientId("client") + .clientSecret("secret") + .build(); + + @Test + public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() { + this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class).autowire(); + + WebTestClient webTestClient = WebTestClientBuilder + .bindToWebFilters(this.springSecurity) + .build(); + + WebDriver driver = WebTestClientHtmlUnitDriverBuilder + .webTestClientSetup(webTestClient) + .build(); + + FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage + .to(driver, FormLoginTests.DefaultLoginPage.class) + .assertAt() + .assertLoginFormNotPresent() + .oauth2Login() + .assertClientRegistrationByName(this.github.getClientName()) + .and(); + } + + @EnableWebFluxSecurity + static class OAuth2LoginWithMulitpleClientRegistrations { + @Bean + InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() { + ClientRegistration google = CommonOAuth2Provider.GOOGLE + .getBuilder("google") + .clientId("client") + .clientSecret("secret") + .build(); + return new InMemoryReactiveClientRegistrationRepository(github, google); + } + } + + @Test + public void defaultLoginPageWithSingleClientRegistrationThenRedirect() { + this.spring.register(OAuth2LoginWithSingleClientRegistrations.class).autowire(); + + WebTestClient webTestClient = WebTestClientBuilder + .bindToWebFilters(new GitHubWebFilter(), this.springSecurity) + .build(); + + WebDriver driver = WebTestClientHtmlUnitDriverBuilder + .webTestClientSetup(webTestClient) + .build(); + + driver.get("http://localhost/"); + + assertThat(driver.getCurrentUrl()).startsWith("https://github.com/login/oauth/authorize"); + } + + @EnableWebFluxSecurity + static class OAuth2LoginWithSingleClientRegistrations { + @Bean + InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(github); + } + } + + static class GitHubWebFilter implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + if (exchange.getRequest().getURI().getHost().equals("github.com")) { + return exchange.getResponse().setComplete(); + } + return chain.filter(exchange); + } + } +} 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 8926888bdb5..5e572c559ff 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 @@ -40,7 +40,6 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; /** * @author Rob Winch @@ -92,12 +91,9 @@ public void basic() { WebTestClient client = buildClient(); - EntityExchangeResult result = client - .mutate() - .filter(basicAuthentication("rob", "rob")) - .build() - .get() + EntityExchangeResult result = client.get() .uri("/") + .headers(headers -> headers.setBasicAuth("rob", "rob")) .exchange() .expectStatus().isOk() .expectHeader().valueMatches(HttpHeaders.CACHE_CONTROL, ".+") diff --git a/config/src/test/java/org/springframework/security/config/web/server/TestingServerHttpSecurity.java b/config/src/test/java/org/springframework/security/config/web/server/TestingServerHttpSecurity.java new file mode 100644 index 00000000000..c36d75e9d60 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/web/server/TestingServerHttpSecurity.java @@ -0,0 +1,32 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://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.beans.BeansException; +import org.springframework.context.ApplicationContext; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class TestingServerHttpSecurity extends ServerHttpSecurity { + public TestingServerHttpSecurity applicationContext(ApplicationContext applicationContext) + throws BeansException { + super.setApplicationContext(applicationContext); + return this; + } +} diff --git a/config/src/test/java/org/springframework/security/htmlunit/server/HtmlUnitWebTestClient.java b/config/src/test/java/org/springframework/security/htmlunit/server/HtmlUnitWebTestClient.java index f9dfab20929..111bd62c31f 100644 --- a/config/src/test/java/org/springframework/security/htmlunit/server/HtmlUnitWebTestClient.java +++ b/config/src/test/java/org/springframework/security/htmlunit/server/HtmlUnitWebTestClient.java @@ -151,8 +151,14 @@ public Mono filter(ClientRequest request, ExchangeFunction next) private Mono redirectIfNecessary(ClientRequest request, ExchangeFunction next, ClientResponse response) { URI location = response.headers().asHttpHeaders().getLocation(); + String host = request.url().getHost(); + String scheme = request.url().getScheme(); if(location != null) { - ClientRequest redirect = ClientRequest.method(HttpMethod.GET, URI.create("http://localhost" + location.toASCIIString())) + String redirectUrl = location.toASCIIString(); + if (location.getHost() == null) { + redirectUrl = scheme+ "://" + host + location.toASCIIString(); + } + ClientRequest redirect = ClientRequest.method(HttpMethod.GET, URI.create(redirectUrl)) .headers(headers -> headers.addAll(request.headers())) .cookies(cookies -> cookies.addAll(request.cookies())) .attributes(attributes -> attributes.putAll(request.attributes())) diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Default.jwks b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Default.jwks new file mode 100644 index 00000000000..ce5e6fbf2b4 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Default.jwks @@ -0,0 +1 @@ +{"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"}]} diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Empty.jwks b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Empty.jwks new file mode 100644 index 00000000000..9d15e791b4f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Empty.jwks @@ -0,0 +1 @@ +{"keys":[]} diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Expired.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Expired.token new file mode 100644 index 00000000000..8010d048938 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Expired.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE1MzAyMzE3MTB9.c8vXYFwe1cBuglaZbmZFXJOmLsu_IQf-OsOiiOGhEJYOzu6h6v_qEzf2xxbu5TSvwAERmDITUSK41UIIvgU75WebtgilNnTR83B_gPM-7_FI2FLzlgVH7WayzvbYTQqepE_ZUMLFkGkK4r-dRiOyB9_cfl6jq_b5hE_biH1qrgPQrjlEhU8YxeK2EE05wsARLzyjoIYifkStjPC6rC-MLFIVk5JoITNzkTh7zYYSWtKWEgwd8S_vluVtJaPk-yKPb4tXcFRzCFl_qd7aCF8_LHyhw-4wvhWRIi8DmQmRU_a1RxR0mi-UCp0jMwmBZxxkSdqJ4l_EHI1yVqpgnbMLDw diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ExpiresAt4687177990.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ExpiresAt4687177990.token new file mode 100644 index 00000000000..df5ab8ac23a --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ExpiresAt4687177990.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjQ2ODcxNzc5OTB9.RRQvqIZzLweq0iwWUZk1Dpiz6iUmT4bAVhGWqvWNWK3UwJ6aBIYsCRhdVeKQp-g1TxXovMALeAu_2oPmV0wOEEanesAKxjKYcJZQIe8HnVqgug6Ibs04uQ1mJ4RgfntPM-ebsJs-2tjFFkLEYJSkpq2o6SEFW9jBJyW8b8C5UJJahqynonA-Dw5GH1nin5bhhliLuFOmu0Ityt0uJ1Y_vuGsSA-ltVcY52jE4x6GH9NQxLX4ceO1bHSOmdspBoGsE_yo9-zsQw0g1_Iy7uqEjos3xrrboH6Z_u7pRL7AQJ7GNzZlinjYYPANQbYknieZD6beddTK7lvr4DYiPBmXzA diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Kid.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Kid.token new file mode 100644 index 00000000000..4f53fdea73f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Kid.token @@ -0,0 +1 @@ +eyJraWQiOiJvbmUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjQ2ODM4ODM1NzJ9.UhukjNEowC5lLCccvdjCUJad5J9FGNModegMZGe9qKIbXxmfseTttZUNn3_K_6aNCfimtmRktCRbw3fUTcje2TFJOJ6SmomLcQyjq7S41Wq6oBSA2fdqOOU4vNvrk8_pSExsSyN9bfWiJ51I8Agzbq5eUDNo_HEpaJZimrIe9f2_njU1GxvAWsq_h4UhHEgPPb3kY9kN9hVYX_oShhh7JxbLJBnfsKBOKGEWOsE65GlmDgQV4om6RGjJaz6jFHKJTCpH08ADA3j2dqT0LNy4PrUmbnjPjWVtSQJkGcgUkcQW6qz0K86ZfJZZng_iB2VadRm5qO-99ySKmlxa5A-_Iw diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-MalformedPayload.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-MalformedPayload.token new file mode 100644 index 00000000000..a6c9be5a210 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-MalformedPayload.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODM4OTEyMTl9.kpdv6ZXyYszZUzA4mJpviCBPzPftk6tIbIn5OoMuM09MKZCUCAFD8Y1tDmjzbWdkR_5CYiFMvSLq6DzAlugtGRAShc93dmDlyZmhcct2G477FxWaRKbtmFDjzuCjGyn7xHWpS7Wz6-Ngb-JyGI2m7FxXCgCpiYYBl-4-ONTuAT0fArJi_voA8K6YLnnjEjEprI3wsQRoS3Twa_fVdGkpMNlOGsQOqmlfjDrXpyfiANOe_ZztHxbDtJEZ9zfELxx9fzkZgTL1fD2Sj6HueDU-tMt-6IaGpBCLsg7d85RK001-U9u3Ph9awQC4QZK-8-F9OUUCY5RNcRJ57KEh9PjUfA diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TooEarly.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TooEarly.token new file mode 100644 index 00000000000..780cf108364 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TooEarly.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOjQ2ODM4OTI2NTUsImV4cCI6NDY4Mzg5MjY1NX0.MIaECJrmYjAByKNJoWHlP5ewg2xiW7GIxL8Vepp3ZIKf_jjM2OSMQlAWGmfD3Kf3bfesvSI7glw5qg_ZIv4FdIPaTvnmLRjWQkpk-QiLTJr_HM2wWeNbUJ1zciGWQlWAvabtQuyeGt1dsfQq53QLVNpvuioYdVg-gz_76uwDTxCKQU_99ksQhMMJsYJVDA_-uWGTzBANszcZykqwWFMaoXF4lkVPK4U68n18ISBB761wFusUCtyGWzwevX7wBAEJxcRy6ZVk3h7GyxZBsbRAd5fPn3dPMxNvL_CEp5jUYSAH-arAdDkvAph5Vk1yXof7FFRcffJpAy76HC66hR2JQA diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TwoKeys.jwks b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TwoKeys.jwks new file mode 100644 index 00000000000..16d3a00859f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-TwoKeys.jwks @@ -0,0 +1 @@ +{"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"},{"p":"_CI5g5In9T4ZgakV1i62UU6yjorEr5t2URHfRYqxN7S4aKsQOzggcPoqa78xRj8PAPuf3P0ArPEAHdS6bFK7RLrFXdvyEmSNTJa1gcLCf2Zmep8bsrhrCvh6seZNvfrSMV0ULmk0B75Fs8mqE7nwcIbPtBYkinlSIw-sKRv62DM","kty":"RSA","q":"pqfexT3HBAagH-iydGsWbjG6CcYyvSQZdFtUu4LIOBCYVA0dvkN9s7uU1eoevHN_ksf-hfrF5AQH0a5P0dIJ2pp1bFa9uo9DJ7khU9sIBk9_o8nST2QLHwPQmGTW8vVlcSF7Vffvzm2fV3cQ3dfI5lvtkqfX_Z3WkF8UjFjADe8","d":"FzB5xChO8e89JisxSueY5j1RUBmatIAs_8Z3LUHOw16GlAhBhbSNl-7bXkbcUWLq9M1zTLCD91SSZXBohf9j1ebqWnbjMqQmdkxlQcVRoKcnMJ5YBabCTMBXghQnJetUMh6x6hXRnR1CSBNRdZPf-K2bnxL3xRNRSfY_7bjpb_q5pyUsK66ugSKwuEOUDNf1ttOZi4PBTsxWMDyXi_7fNFjl-B831uWNDVwdY4j68PVwGPT87zjZYjZRTZXB4ILUP11ztw4s3s_bU1Lj0PeZJsA5rmjU1iBzqCNdzgYxNlfV7M62VCkE1Wtd6M97jtysiT-5wQUMxNugoOTc9thc1Q","e":"AQAB","use":"sig","kid":"two","qi":"bnGriiVGVea9vSaN_48YYTEoKYM1kF7TrCRKERkMWdi4EHF7pZNWBv8arxaLUzElllvtGlVTNwkZlG0gOhXBoLYbcfqVikDklkBxtsuZEBKgvX7zFlDIBlNjh98lcZqDqz7Rqwr-tavxTCq2LNNlK6x-dYL61Agw_LOilYqbSfA","dp":"MmT4z-ZnnCn0WSkdlziw8iFjqP_tfhf5lwyWbsTg1PyHG0yNqvh1637k-bI2PA8ghZbFhhr_hpGI7210cXA7w-n8xtzOToTQhS1eS_hMfcBO3VVt6NPZeVDe3S3l_gHi_0DWZsxaPO336o51MwooF6WqYBlI5nCHTUC1rWXNRmc","dq":"dd_ybywc4boV87vQzQsZWGOPpG4tYR5xap1WtzHvj8gdFgYY7YQrGr8orIzlpIFE0Hroibcv1PEM3sAd8NhQ4--v8isAEz5VT3lgG0Gm0V_VdfG_8StfulYmakOYzUvIrlXyOIIfebCLrX-nzGFd1aFbzgktelLzejXmAMadQL0","n":"pCOHBsaoxlt9-qVE_INhrbkmxm7WqwEeqUBBIgHvm_JzXbmJ4iQzVF5tzAbRayxUmPbZ4E80R5HlIC2CQ7yyweTbIIWIw_TcQzXR4u3twEN1awP4s1n-00Eeurr-s9c_txZQQiDkyrCMYc9vlmsneFfubyoTvg9h_rckd8w34AyE8-wxgBRqUbm1x4ozcVmUJHkaPbQfbhIighl7osoQ4t_wXjAhTN_c9XttVjXlRwqVYPFNYUcC9GoaXWJRHjydHNFeBboOZY3E8ND6DbJ4nVtxydpUQSjTC-N-wQmhKmtYadd2hh2yywvtXpL5Q98XSphrrIHK-GWY0j8kimpunQ"}]} diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Unsigned.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Unsigned.token new file mode 100644 index 00000000000..f0b557652d3 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-Unsigned.token @@ -0,0 +1 @@ +eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJuYmYiOjE1MzAzMDA4MzgsImV4cCI6MjE0NjAwMzE5OSwiaWF0IjoxNTMwMzAwODM4LCJ0eXAiOiJKV1QifQ. diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScope.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScope.token new file mode 100644 index 00000000000..0020772ffe3 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScope.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6NDY4Mzg4MzIxMX0.cM7Eq9H20503czYVy1aVo8MqTQd8YsYGpv_lAV4PKr3y8NgvvosNjCSUs8rrGjQ0Sp3c4iXK6UVXq8pOJVeWXbSZa1IKAsIhiMIcg2xPFM6e71MVdX4bo255Yh8Nuh0p3xxP9isK_iAKNdMuVBOGfe9KATlmp2dOi0OpAjwSmxPJD1A7AC5f62YIe3Yx2gO6mbfANZJWQ7TxlUuCT_D5FEqg2FfYFqlFaluqWd_2X-esIsiDTxa1R9oF5XwgT6tsgvS7iYSiJw_uNKX0yU4eyLzYuIhnN_hVsr4jOZqPlsqCrkEohOGZg_Jir-7tLxZu0PqoH4ejC24FeDtC9xVa0w diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScp.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScp.token new file mode 100644 index 00000000000..3c2a2811527 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageReadScp.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTpyZWFkIl0sImV4cCI6NDY4Mzg5Nzc3Nn0.LtMVtIiRIwSyc3aX35Zl0JVwLTcQZAB3dyBOMHNaHCKUljwMrf20a_gT79LfhjDzE_fUVUmFiAO32W1vFnYpZSVaMDUgeIOIOpxfoe9shj_uYenAwIS-_UxqGVIJiJoXNZh_MK80ShNpvsQwamxWEEOAMBtpWNiVYNDMdfgho9n3o5_Z7Gjy8RLBo1tbDREbO9kTFwGIxm_EYpezmRCRq4w1DdS6UDW321hkwMxPnCMSWOvp-hRpmgY2yjzLgPJ6Aucmg9TJ8jloAP1DjJoF1gRR7NTAk8LOGkSjTzVYDYMbCF51YdpojhItSk80YzXiEsv1mTz4oMM49jXBmfXFMA diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageWriteScp.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageWriteScp.token new file mode 100644 index 00000000000..7cdb29ea59e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidMessageWriteScp.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTp3cml0ZSJdLCJleHAiOjQ2ODM4OTY0OTl9.mxAFzoNjjo-7E4D_XYVme69Y7F-J--q41x6lHDTSOxzVNfQqtJ-U-N4pn7St5jElm9y3mSUxTtmwCnukaVVZkeI8aJjUc8V8nxUAsiZIDvQWjr9uW4xUIcE6MiwC0A9rhY-3I87u6No-KBTxyT80zLnCjtS2XpTId-NSd3vcYmM7Vzn4-8KoR_m-7XrjvrO69HlRrH2uUAXGnr1sn6vLp7YruupqKrHqa0e9pIpN-VRzC8Bx2LQP9mVMlQy4b1hx5MdjOTV3HUSnWiT-93z4rTMOoHScKDwmzFYoS7e00F5hyd4jzbpHdpDKnjLdwPQYz_HCmQ5MV21-Q4Q1jparIg diff --git a/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidNoScopes.token b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidNoScopes.token new file mode 100644 index 00000000000..7d4a3251d24 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests-ValidNoScopes.token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjQ2ODM4Mjg2NzR9.LV_i9lzN_gAB2MUuZHJKm2tOfa3xWq_qfE2lx67eoYJZsY_20Ma98A3Hh2k0wnb_mNn6jfQhXbqvUy1llmQtsx3gMNhN2Axfe3UccSKYEb2Ow5OFlrMFYby1d_D4GfXKUFKq8jyMWVlrjk_XrfJyfzeo0MyZVzURSOXv1Ehbl5-xAS_N72jiAI7cIHlHGm93Hwdk8h7Tkkf_5t2dOMJM0mh0fOT9ou3J2_ngaNDfvlAmBLxHQiJ6JrFH5njqe4lSBTxJocDcgZwGVKd0WvV4W-jwA267tZjssDFmS3xZ9hoDO_M-EjlOiEPuWLd9nQCGJpBJ3z3WeC4qrKYghHTNLA diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-AutoConfig.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-AutoConfig.xml index 44959cf8d9e..644f4a8d744 100644 --- a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-AutoConfig.xml +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-AutoConfig.xml @@ -18,10 +18,15 @@ + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfEnabled.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfEnabled.xml index 3a09d6e370e..001214c123b 100644 --- a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfEnabled.xml +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-CsrfEnabled.xml @@ -18,13 +18,18 @@ + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests-AutoConfig.xml b/config/src/test/resources/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests-AutoConfig.xml new file mode 100644 index 00000000000..13372dd9441 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/FormLoginBeanDefinitionParserTests-AutoConfig.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-Minimal.xml b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-Minimal.xml new file mode 100644 index 00000000000..1db9eff2276 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpConfigTests-Minimal.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-RequiresMvc.xml b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-RequiresMvc.xml new file mode 100644 index 00000000000..9a66ab3a5b5 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-RequiresMvc.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCors.xml b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCors.xml new file mode 100644 index 00000000000..ac57e95b45b --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCors.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsConfigurationSource.xml b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsConfigurationSource.xml new file mode 100644 index 00000000000..b73f227634d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsConfigurationSource.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsFilter.xml b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsFilter.xml new file mode 100644 index 00000000000..28daed34b1f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpCorsConfigTests-WithCorsFilter.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-CacheControlDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-CacheControlDisabled.xml new file mode 100644 index 00000000000..8cc3f97354f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-CacheControlDisabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithEmptyDirectives.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithEmptyDirectives.xml new file mode 100644 index 00000000000..b6c64a228b8 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithEmptyDirectives.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithPolicyDirectives.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithPolicyDirectives.xml new file mode 100644 index 00000000000..ff3eee270ac --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithPolicyDirectives.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithReportOnly.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithReportOnly.xml new file mode 100644 index 00000000000..55ae7559b80 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentSecurityPolicyWithReportOnly.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentTypeOptionsDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentTypeOptionsDisabled.xml new file mode 100644 index 00000000000..6e2d415bf63 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-ContentTypeOptionsDisabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultConfig.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultConfig.xml new file mode 100644 index 00000000000..2cacad9a183 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultConfig.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCacheControl.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCacheControl.xml new file mode 100644 index 00000000000..1692f68fa0d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCacheControl.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentSecurityPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentSecurityPolicy.xml new file mode 100644 index 00000000000..bcc2a6632c3 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentSecurityPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentTypeOptions.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentTypeOptions.xml new file mode 100644 index 00000000000..ee6f20a0a1a --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithContentTypeOptions.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeader.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeader.xml new file mode 100644 index 00000000000..31c6f8d3059 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeader.xml @@ -0,0 +1,37 @@ + + + + + + + +
        +
        + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeaderWriter.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeaderWriter.xml new file mode 100644 index 00000000000..1991d74152c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHeaderWriter.xml @@ -0,0 +1,41 @@ + + + + + + + +
        + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHstsRequestMatcher.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHstsRequestMatcher.xml new file mode 100644 index 00000000000..8fa625c8aa3 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCustomHstsRequestMatcher.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyHpkp.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyHpkp.xml new file mode 100644 index 00000000000..6249be2c170 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyHpkp.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyPins.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyPins.xml new file mode 100644 index 00000000000..a37f9e7221c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithEmptyPins.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptions.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptions.xml new file mode 100644 index 00000000000..9fc1c786632 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptions.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFrom.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFrom.xml new file mode 100644 index 00000000000..481c0bc3797 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFrom.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromBlankOrigin.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromBlankOrigin.xml new file mode 100644 index 00000000000..3be6ee362e7 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromBlankOrigin.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromNoOrigin.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromNoOrigin.xml new file mode 100644 index 00000000000..47fd5818907 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromNoOrigin.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromWhitelist.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromWhitelist.xml new file mode 100644 index 00000000000..8678f8cb845 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsAllowFromWhitelist.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsDeny.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsDeny.xml new file mode 100644 index 00000000000..3bd66acb256 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsDeny.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsSameOrigin.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsSameOrigin.xml new file mode 100644 index 00000000000..dfac109f130 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithFrameOptionsSameOrigin.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkp.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkp.xml new file mode 100644 index 00000000000..b6875c77e30 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkp.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpDefaults.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpDefaults.xml new file mode 100644 index 00000000000..8c5b07c97bc --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpDefaults.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpIncludeSubdomains.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpIncludeSubdomains.xml new file mode 100644 index 00000000000..f9a911fa127 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpIncludeSubdomains.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpMaxAge.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpMaxAge.xml new file mode 100644 index 00000000000..d1ee8f8ea0c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpMaxAge.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReport.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReport.xml new file mode 100644 index 00000000000..e5d37074e7f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReport.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReportUri.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReportUri.xml new file mode 100644 index 00000000000..3fa18da5a69 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHpkpReportUri.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHsts.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHsts.xml new file mode 100644 index 00000000000..6e1cebc7896 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithHsts.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithNoOverride.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithNoOverride.xml new file mode 100644 index 00000000000..f7619328d44 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithNoOverride.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderName.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderName.xml new file mode 100644 index 00000000000..7978bd08c3e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderName.xml @@ -0,0 +1,36 @@ + + + + + + + +
        + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderValue.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderValue.xml new file mode 100644 index 00000000000..fba21c98de1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithOnlyHeaderValue.xml @@ -0,0 +1,36 @@ + + + + + + + +
        + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicy.xml new file mode 100644 index 00000000000..b9f6b38600b --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicySameOrigin.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicySameOrigin.xml new file mode 100644 index 00000000000..a7b319576de --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithReferrerPolicySameOrigin.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtection.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtection.xml new file mode 100644 index 00000000000..dfabc869805 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtection.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabled.xml new file mode 100644 index 00000000000..50c01fe7d3e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabledAndBlockSet.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabledAndBlockSet.xml new file mode 100644 index 00000000000..b648cde25a1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionDisabledAndBlockSet.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionEnabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionEnabled.xml new file mode 100644 index 00000000000..cfa732f97b4 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithXssProtectionEnabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabled.xml new file mode 100644 index 00000000000..6ab1b5fa066 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabledSpecifyingPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabledSpecifyingPolicy.xml new file mode 100644 index 00000000000..73e94a3c87d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-FrameOptionsDisabledSpecifyingPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabled.xml new file mode 100644 index 00000000000..802f31eaff6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabled.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledHavingChildElement.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledHavingChildElement.xml new file mode 100644 index 00000000000..4946f505e97 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledHavingChildElement.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledWithContentSecurityPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledWithContentSecurityPolicy.xml new file mode 100644 index 00000000000..64b7287c04c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersDisabledWithContentSecurityPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersEnabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersEnabled.xml new file mode 100644 index 00000000000..fab128bd329 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HeadersEnabled.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HpkpDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HpkpDisabled.xml new file mode 100644 index 00000000000..1b13db83d69 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HpkpDisabled.xml @@ -0,0 +1,40 @@ + + + + + + + + + + d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM= + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabled.xml new file mode 100644 index 00000000000..6bc89f24ca6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingIncludeSubdomains.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingIncludeSubdomains.xml new file mode 100644 index 00000000000..4fa83aa5d9e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingIncludeSubdomains.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingMaxAge.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingMaxAge.xml new file mode 100644 index 00000000000..b23dd301a4f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingMaxAge.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingRequestMatcher.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingRequestMatcher.xml new file mode 100644 index 00000000000..903f96c137d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-HstsDisabledSpecifyingRequestMatcher.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-WithFrameOptions.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-WithFrameOptions.xml new file mode 100644 index 00000000000..cbee408f6f7 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-WithFrameOptions.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabled.xml new file mode 100644 index 00000000000..5b2ac369f2f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledAndEnabled.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledAndEnabled.xml new file mode 100644 index 00000000000..c590b69dea0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledAndEnabled.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledSpecifyingBlock.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledSpecifyingBlock.xml new file mode 100644 index 00000000000..c183c36d953 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-XssProtectionDisabledSpecifyingBlock.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-AntMatcherServletPath.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-AntMatcherServletPath.xml new file mode 100644 index 00000000000..c1ddd45cf74 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-AntMatcherServletPath.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CamelCasePathVariables.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CamelCasePathVariables.xml new file mode 100644 index 00000000000..091676e458d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CamelCasePathVariables.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CiRegexMatcherServletPath.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CiRegexMatcherServletPath.xml new file mode 100644 index 00000000000..cb8f4fd6e30 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-CiRegexMatcherServletPath.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-DefaultMatcherServletPath.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-DefaultMatcherServletPath.xml new file mode 100644 index 00000000000..ddf3ebc630f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-DefaultMatcherServletPath.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-HasAnyRole.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-HasAnyRole.xml new file mode 100644 index 00000000000..68e6fe8ebaa --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-HasAnyRole.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchers.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchers.xml new file mode 100644 index 00000000000..041e070b9f1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchers.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersPathVariables.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersPathVariables.xml new file mode 100644 index 00000000000..c7ffba5d9b9 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersPathVariables.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPath.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPath.xml new file mode 100644 index 00000000000..365503eda6c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPath.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PatchMethod.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PatchMethod.xml new file mode 100644 index 00000000000..4227abd0e89 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PatchMethod.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PathVariables.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PathVariables.xml new file mode 100644 index 00000000000..f0ed68089b8 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-PathVariables.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-RegexMatcherServletPath.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-RegexMatcherServletPath.xml new file mode 100644 index 00000000000..9c242663f61 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-RegexMatcherServletPath.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-Sec2256.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-Sec2256.xml new file mode 100644 index 00000000000..756c38fbbaa --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-Sec2256.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-TypeConversionPathVariables.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-TypeConversionPathVariables.xml new file mode 100644 index 00000000000..8d613919749 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-TypeConversionPathVariables.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-DistinctHttpElements.xml b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-DistinctHttpElements.xml new file mode 100644 index 00000000000..be89f7d9771 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-DistinctHttpElements.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticalHttpElements.xml b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticalHttpElements.xml new file mode 100644 index 00000000000..ba1c05489c6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticalHttpElements.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticallyPatternedHttpElements.xml b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticallyPatternedHttpElements.xml new file mode 100644 index 00000000000..e027a52dff2 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-IdenticallyPatternedHttpElements.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-Sec1937.xml b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-Sec1937.xml new file mode 100644 index 00000000000..fbe6d7d5724 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MultiHttpBlockConfigTests-Sec1937.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-Sec2919.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-Sec2919.xml new file mode 100644 index 00000000000..db2e2869759 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-Sec2919.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLogin.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLogin.xml new file mode 100644 index 00000000000..331369ef87c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLogin.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginAndOpenIDLoginPages.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginAndOpenIDLoginPages.xml new file mode 100644 index 00000000000..de9bb8a1346 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginAndOpenIDLoginPages.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginPage.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginPage.xml new file mode 100644 index 00000000000..cf84079b711 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithFormLoginPage.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDAttributes.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDAttributes.xml new file mode 100644 index 00000000000..9edaef345a9 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDAttributes.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDLoginPageAndFormLogin.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDLoginPageAndFormLogin.xml new file mode 100644 index 00000000000..a61ef9af0a2 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithOpenIDLoginPageAndFormLogin.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithRememberMe.xml b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithRememberMe.xml new file mode 100644 index 00000000000..63108937bff --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OpenIDConfigTests-WithRememberMe.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPage.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPage.xml new file mode 100644 index 00000000000..04f10d3d008 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPage.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPageWithSpEL.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPageWithSpEL.xml new file mode 100644 index 00000000000..d4f67486e0d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-AccessDeniedPageWithSpEL.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLogin.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLogin.xml new file mode 100644 index 00000000000..be48c5b85d0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLogin.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLoginWithSpEL.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLoginWithSpEL.xml new file mode 100644 index 00000000000..93e955f7561 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-InterceptUrlAndFormLoginWithSpEL.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-PortMapping.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-PortMapping.xml new file mode 100644 index 00000000000..c135a75b08b --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-PortMapping.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-RequiresChannel.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-RequiresChannel.xml new file mode 100644 index 00000000000..1d9c90488d6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-RequiresChannel.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-UnsecuredPattern.xml b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-UnsecuredPattern.xml new file mode 100644 index 00000000000..139a459c764 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/PlaceHolderAndELConfigTests-UnsecuredPattern.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-DefaultConfig.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-DefaultConfig.xml new file mode 100644 index 00000000000..b59915244ad --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-DefaultConfig.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidity.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidity.xml new file mode 100644 index 00000000000..ced04be82d2 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidity.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithDataSource.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithDataSource.xml new file mode 100644 index 00000000000..a650cda5f61 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithDataSource.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithPersistentRepository.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithPersistentRepository.xml new file mode 100644 index 00000000000..ff41dc27cbb --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-NegativeTokenValidityWithPersistentRepository.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec1827.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec1827.xml new file mode 100644 index 00000000000..76076025d93 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec1827.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec2165.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec2165.xml new file mode 100644 index 00000000000..7874a9bfe35 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec2165.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec742.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec742.xml new file mode 100644 index 00000000000..6d58d63ba50 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-Sec742.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-SecureCookie.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-SecureCookie.xml new file mode 100644 index 00000000000..1c59a2c6536 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-SecureCookie.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-TokenValidity.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-TokenValidity.xml new file mode 100644 index 00000000000..9517b3a791d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-TokenValidity.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithAuthenticationSuccessHandler.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithAuthenticationSuccessHandler.xml new file mode 100644 index 00000000000..360d2004a7a --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithAuthenticationSuccessHandler.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithDataSource.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithDataSource.xml new file mode 100644 index 00000000000..1c0834a88b0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithDataSource.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookie.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookie.xml new file mode 100644 index 00000000000..5e101d025b8 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookie.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookieAndServicesRef.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookieAndServicesRef.xml new file mode 100644 index 00000000000..bcc013e76dc --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeCookieAndServicesRef.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameter.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameter.xml new file mode 100644 index 00000000000..94d8cf8c71e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameter.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameterAndServicesRef.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameterAndServicesRef.xml new file mode 100644 index 00000000000..46dff530c60 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithRememberMeParameterAndServicesRef.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithServicesRef.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithServicesRef.xml new file mode 100644 index 00000000000..682cd2c3ead --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithServicesRef.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithTokenRepository.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithTokenRepository.xml new file mode 100644 index 00000000000..009506769cd --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithTokenRepository.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithUserDetailsService.xml b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithUserDetailsService.xml new file mode 100644 index 00000000000..8b18a9d9664 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/RememberMeConfigTests-WithUserDetailsService.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-FormLogin.xml b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-FormLogin.xml new file mode 100644 index 00000000000..0caf67fac90 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-FormLogin.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-HttpBasic.xml b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-HttpBasic.xml new file mode 100644 index 00000000000..b89d96a67ae --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-HttpBasic.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Logout.xml b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Logout.xml new file mode 100644 index 00000000000..d2b0532fe16 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Logout.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-MultiHttp.xml b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-MultiHttp.xml new file mode 100644 index 00000000000..38d74133a7e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-MultiHttp.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Simple.xml b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Simple.xml new file mode 100644 index 00000000000..8d8e2473b1f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SecurityContextHolderAwareRequestConfigTests-Simple.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlCustomLogoutHandler.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlCustomLogoutHandler.xml new file mode 100644 index 00000000000..a95022eeac6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlCustomLogoutHandler.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlExpiredUrl.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlExpiredUrl.xml new file mode 100644 index 00000000000..38141dfc7f0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlExpiredUrl.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlLogoutAndRememberMeHandlers.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlLogoutAndRememberMeHandlers.xml new file mode 100644 index 00000000000..6f75ce1d2f6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlLogoutAndRememberMeHandlers.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlMaxSessions.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlMaxSessions.xml new file mode 100644 index 00000000000..f16c2da2f87 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlMaxSessions.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlRememberMeHandler.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlRememberMeHandler.xml new file mode 100644 index 00000000000..eec4a40f9b1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlRememberMeHandler.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryAlias.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryAlias.xml new file mode 100644 index 00000000000..13baba1949e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryAlias.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryRef.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryRef.xml new file mode 100644 index 00000000000..057f5985c0c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryRef.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionAlways.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionAlways.xml new file mode 100644 index 00000000000..049c3c47508 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionAlways.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionIfRequired.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionIfRequired.xml new file mode 100644 index 00000000000..47a770d00e8 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionIfRequired.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionNever.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionNever.xml new file mode 100644 index 00000000000..0edce4d62ab --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionNever.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionStateless.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionStateless.xml new file mode 100644 index 00000000000..b252e1f8f2c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionStateless.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-NoSessionManagementFilter.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-NoSessionManagementFilter.xml new file mode 100644 index 00000000000..84944d46753 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-NoSessionManagementFilter.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec1208.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec1208.xml new file mode 100644 index 00000000000..afdedc05200 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec1208.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec2137.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec2137.xml new file mode 100644 index 00000000000..10fd3e2d5b1 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec2137.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionAuthenticationStrategyRef.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionAuthenticationStrategyRef.xml new file mode 100644 index 00000000000..04c2d7a0395 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionAuthenticationStrategyRef.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionMigrateSession.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionMigrateSession.xml new file mode 100644 index 00000000000..e0cb0476823 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionMigrateSession.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNone.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNone.xml new file mode 100644 index 00000000000..217daddcb7f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNone.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNoneWithInvalidSessionUrl.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNoneWithInvalidSessionUrl.xml new file mode 100644 index 00000000000..807cea0710d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNoneWithInvalidSessionUrl.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-CreateSessionAlwaysWithTransientAuthentication.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-CreateSessionAlwaysWithTransientAuthentication.xml new file mode 100644 index 00000000000..804e3dedd42 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-CreateSessionAlwaysWithTransientAuthentication.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-WithTransientAuthentication.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-WithTransientAuthentication.xml new file mode 100644 index 00000000000..ae66dcbbb07 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-WithTransientAuthentication.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/userservice.xml b/config/src/test/resources/org/springframework/security/config/http/userservice.xml index 80314c3acc0..62a7b3f6abf 100644 --- a/config/src/test/resources/org/springframework/security/config/http/userservice.xml +++ b/config/src/test/resources/org/springframework/security/config/http/userservice.xml @@ -23,7 +23,8 @@ http://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - + + diff --git a/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java new file mode 100644 index 00000000000..80676d8a3ac --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.authentication; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * A {@link ReactiveAuthenticationManager} that delegates to other {@link ReactiveAuthenticationManager} instances using + * the result from the first non empty result. + * + * @author Rob Winch + * @since 5.1 + */ +public class DelegatingReactiveAuthenticationManager + implements ReactiveAuthenticationManager { + private final List delegates; + + public DelegatingReactiveAuthenticationManager( + ReactiveAuthenticationManager... entryPoints) { + this(Arrays.asList(entryPoints)); + } + + public DelegatingReactiveAuthenticationManager( + List entryPoints) { + Assert.notEmpty(entryPoints, "entryPoints cannot be null"); + this.delegates = entryPoints; + } + + public Mono authenticate(Authentication authentication) { + return Flux.fromIterable(this.delegates) + .concatMap(m -> m.authenticate(authentication)) + .next(); + } +} diff --git a/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java index 0dbab239782..0a79e6b2446 100644 --- a/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java @@ -18,11 +18,13 @@ import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Assert; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; /** @@ -37,6 +39,10 @@ public class UserDetailsRepositoryReactiveAuthenticationManager implements React private PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + private ReactiveUserDetailsPasswordService userDetailsPasswordService; + + private Scheduler scheduler = Schedulers.parallel(); + public UserDetailsRepositoryReactiveAuthenticationManager(ReactiveUserDetailsService userDetailsService) { Assert.notNull(userDetailsService, "userDetailsService cannot be null"); this.userDetailsService = userDetailsService; @@ -45,11 +51,21 @@ public UserDetailsRepositoryReactiveAuthenticationManager(ReactiveUserDetailsSer @Override public Mono authenticate(Authentication authentication) { final String username = authentication.getName(); + final String presentedPassword = (String) authentication.getCredentials(); return this.userDetailsService.findByUsername(username) - .publishOn(Schedulers.parallel()) - .filter( u -> this.passwordEncoder.matches((String) authentication.getCredentials(), u.getPassword())) + .publishOn(this.scheduler) + .filter(u -> this.passwordEncoder.matches(presentedPassword, u.getPassword())) .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials")))) - .map( u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()) ); + .flatMap(u -> { + boolean upgradeEncoding = this.userDetailsPasswordService != null + && this.passwordEncoder.upgradeEncoding(u.getPassword()); + if (upgradeEncoding) { + String newPassword = this.passwordEncoder.encode(presentedPassword); + return this.userDetailsPasswordService.updatePassword(u, newPassword); + } + return Mono.just(u); + }) + .map(u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()) ); } /** @@ -61,4 +77,29 @@ public void setPasswordEncoder(PasswordEncoder passwordEncoder) { Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); this.passwordEncoder = passwordEncoder; } + + /** + * Sets the {@link Scheduler} used by the {@link UserDetailsRepositoryReactiveAuthenticationManager}. + * The default is {@code Schedulers.parallel()} because modern password encoding is + * a CPU intensive task that is non blocking. This means validation is bounded by the + * number of CPUs. Some applications may want to customize the {@link Scheduler}. For + * example, if users are stuck using the insecure {@link org.springframework.security.crypto.password.NoOpPasswordEncoder} + * they might want to leverage {@code Schedulers.immediate()}. + * + * @param scheduler the {@link Scheduler} to use. Cannot be null. + * @since 5.0.6 + */ + public void setScheduler(Scheduler scheduler) { + Assert.notNull(scheduler, "scheduler cannot be null"); + this.scheduler = scheduler; + } + + /** + * Sets the service to use for upgrading passwords on successful authentication. + * @param userDetailsPasswordService the service to use + */ + public void setUserDetailsPasswordService( + ReactiveUserDetailsPasswordService userDetailsPasswordService) { + this.userDetailsPasswordService = userDetailsPasswordService; + } } diff --git a/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java index 07c34e0a934..0c4e86b574a 100644 --- a/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java @@ -20,12 +20,14 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; import org.springframework.util.Assert; /** @@ -62,6 +64,8 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication private UserDetailsService userDetailsService; + private UserDetailsPasswordService userDetailsPasswordService; + public DaoAuthenticationProvider() { setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()); } @@ -120,6 +124,19 @@ protected final UserDetails retrieveUser(String username, } } + @Override + protected Authentication createSuccessAuthentication(Object principal, + Authentication authentication, UserDetails user) { + boolean upgradeEncoding = this.userDetailsPasswordService != null + && this.passwordEncoder.upgradeEncoding(user.getPassword()); + if (upgradeEncoding) { + String presentedPassword = authentication.getCredentials().toString(); + String newPassword = this.passwordEncoder.encode(presentedPassword); + user = this.userDetailsPasswordService.updatePassword(user, newPassword); + } + return super.createSuccessAuthentication(principal, authentication, user); + } + private void prepareTimingAttackProtection() { if (this.userNotFoundEncodedPassword == null) { this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD); @@ -157,4 +174,9 @@ public void setUserDetailsService(UserDetailsService userDetailsService) { protected UserDetailsService getUserDetailsService() { return userDetailsService; } + + public void setUserDetailsPasswordService( + UserDetailsPasswordService userDetailsPasswordService) { + this.userDetailsPasswordService = userDetailsPasswordService; + } } 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 ad007dbe19f..6e5debaac78 100644 --- a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java +++ b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java @@ -110,7 +110,7 @@ private static String getSpringVersion() { Properties properties = new Properties(); try { properties.load(SpringSecurityCoreVersion.class.getClassLoader().getResourceAsStream("META-INF/spring-security.versions")); - } catch (IOException e) { + } catch (IOException | NullPointerException e) { return null; } return properties.getProperty("org.springframework:spring-core"); diff --git a/core/src/main/java/org/springframework/security/core/Transient.java b/core/src/main/java/org/springframework/security/core/Transient.java new file mode 100644 index 00000000000..785bacc5d60 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/Transient.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A marker for {@link Authentication}s that should never be stored across requests, for example + * a bearer token authentication + * + * @author Josh Cummings + * @since 5.1 + */ +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface Transient { +} diff --git a/core/src/main/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsService.java b/core/src/main/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsService.java index ce44d886cd0..45923a1e9f0 100644 --- a/core/src/main/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsService.java +++ b/core/src/main/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsService.java @@ -31,7 +31,7 @@ * @author Rob Winch * @since 5.0 */ -public class MapReactiveUserDetailsService implements ReactiveUserDetailsService { +public class MapReactiveUserDetailsService implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService { private final Map users; /** @@ -66,6 +66,20 @@ public Mono findByUsername(String username) { return result == null ? Mono.empty() : Mono.just(User.withUserDetails(result).build()); } + @Override + public Mono updatePassword(UserDetails user, String newPassword) { + return Mono.just(user) + .map(u -> + User.withUserDetails(u) + .password(newPassword) + .build() + ) + .doOnNext(u -> { + String key = getKey(user.getUsername()); + this.users.put(key, u); + }); + } + private String getKey(String username) { return username.toLowerCase(); } diff --git a/core/src/main/java/org/springframework/security/core/userdetails/ReactiveUserDetailsPasswordService.java b/core/src/main/java/org/springframework/security/core/userdetails/ReactiveUserDetailsPasswordService.java new file mode 100644 index 00000000000..1dcadf719a6 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/userdetails/ReactiveUserDetailsPasswordService.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.core.userdetails; + +import reactor.core.publisher.Mono; + +/** + * An API for changing a {@link UserDetails} password. + * @author Rob Winch + * @since 5.1 + */ +public interface ReactiveUserDetailsPasswordService { + + /** + * Modify the specified user's password. This should change the user's password in the + * persistent user repository (datbase, LDAP etc). + * + * @param user the user to modify the password for + * @param newPassword the password to change to + * @return the updated UserDetails with the new password + */ + Mono updatePassword(UserDetails user, String newPassword); +} diff --git a/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsPasswordService.java b/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsPasswordService.java new file mode 100644 index 00000000000..a282db973d4 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/userdetails/UserDetailsPasswordService.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.core.userdetails; + +/** + * An API for changing a {@link UserDetails} password. + * @author Rob Winch + * @since 5.1 + */ +public interface UserDetailsPasswordService { + + /** + * Modify the specified user's password. This should change the user's password in the + * persistent user repository (datbase, LDAP etc). + * + * @param user the user to modify the password for + * @param newPassword the password to change to + * @return the updated UserDetails with the new password + */ + UserDetails updatePassword(UserDetails user, String newPassword); +} diff --git a/core/src/main/java/org/springframework/security/provisioning/InMemoryUserDetailsManager.java b/core/src/main/java/org/springframework/security/provisioning/InMemoryUserDetailsManager.java index 6592d63b40f..55c462cde41 100644 --- a/core/src/main/java/org/springframework/security/provisioning/InMemoryUserDetailsManager.java +++ b/core/src/main/java/org/springframework/security/provisioning/InMemoryUserDetailsManager.java @@ -30,6 +30,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.memory.UserAttribute; import org.springframework.security.core.userdetails.memory.UserAttributeEditor; @@ -45,7 +46,8 @@ * @author Luke Taylor * @since 3.1 */ -public class InMemoryUserDetailsManager implements UserDetailsManager { +public class InMemoryUserDetailsManager implements UserDetailsManager, + UserDetailsPasswordService { protected final Log logger = LogFactory.getLog(getClass()); private final Map users = new HashMap<>(); @@ -138,6 +140,14 @@ public void changePassword(String oldPassword, String newPassword) { user.setPassword(newPassword); } + @Override + public UserDetails updatePassword(UserDetails user, String newPassword) { + String username = user.getUsername(); + MutableUserDetails mutableUser = this.users.get(username); + mutableUser.setPassword(newPassword); + return mutableUser; + } + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserDetails user = users.get(username.toLowerCase()); diff --git a/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java b/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java index 7dbe8b61b4f..3857c1f09fe 100644 --- a/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java +++ b/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java @@ -37,6 +37,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import javax.sql.DataSource; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -122,6 +123,13 @@ public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsMa private UserCache userCache = new NullUserCache(); + public JdbcUserDetailsManager() { + } + + public JdbcUserDetailsManager(DataSource dataSource) { + setDataSource(dataSource); + } + // ~ Methods // ======================================================================================================== 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 1ea4dbb6733..fac5c1e8692 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 @@ -52,7 +52,7 @@ public void testDoesNotReturnAdditionalAuthoritiesIfCalledWithoutARunAsSetting() Authentication resultingToken = runAs.buildRunAs(inputToken, new Object(), SecurityConfig.createList("SOMETHING_WE_IGNORE")); - assertThat(resultingToken).isEqualTo(null); + assertThat(resultingToken).isNull(); } @Test diff --git a/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java new file mode 100644 index 00000000000..3ac64b93dc3 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.authentication; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.core.Authentication; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class DelegatingReactiveAuthenticationManagerTests { + @Mock + ReactiveAuthenticationManager delegate1; + + @Mock + ReactiveAuthenticationManager delegate2; + + @Mock + Authentication authentication; + + @Test + public void authenticateWhenEmptyAndNotThenReturnsNotEmpty() { + when(this.delegate1.authenticate(any())).thenReturn(Mono.empty()); + when(this.delegate2.authenticate(any())).thenReturn(Mono.just(this.authentication)); + + DelegatingReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1, this.delegate2); + + assertThat(manager.authenticate(this.authentication).block()).isEqualTo(this.authentication); + } + + @Test + public void authenticateWhenNotEmptyThenOtherDelegatesNotSubscribed() { + // delay to try and force delegate2 to finish (i.e. make sure we didn't use flatMap) + when(this.delegate1.authenticate(any())).thenReturn(Mono.just(this.authentication).delayElement(Duration.ofMillis(100))); + + DelegatingReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1, this.delegate2); + + StepVerifier.create(manager.authenticate(this.authentication)) + .expectNext(this.authentication) + .verifyComplete(); + } + + @Test + public void authenticateWhenBadCredentialsThenDelegate2NotInvokedAndError() { + when(this.delegate1.authenticate(any())).thenReturn(Mono.error(new BadCredentialsException("Test"))); + + DelegatingReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1, this.delegate2); + + StepVerifier.create(manager.authenticate(this.authentication)) + .expectError(BadCredentialsException.class) + .verify(); + } +} diff --git a/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java new file mode 100644 index 00000000000..2037bfef6e9 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.authentication; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class UserDetailsRepositoryReactiveAuthenticationManagerTests { + @Mock + private ReactiveUserDetailsService userDetailsService; + + @Mock + private PasswordEncoder encoder; + + @Mock + private ReactiveUserDetailsPasswordService userDetailsPasswordService; + + @Mock + private Scheduler scheduler; + + private UserDetails user = User.withUsername("user") + .password("password") + .roles("USER") + .build(); + + private UserDetailsRepositoryReactiveAuthenticationManager manager; + + @Before + public void setup() { + this.manager = new UserDetailsRepositoryReactiveAuthenticationManager(this.userDetailsService); + when(this.scheduler.schedule(any())).thenAnswer(a -> { + Runnable r = a.getArgument(0); + return Schedulers.immediate().schedule(r); + }); + } + + @Test + public void setSchedulerWhenNullThenIllegalArgumentException() { + assertThatCode(() -> this.manager.setScheduler(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void authentiateWhenCustomSchedulerThenUsed() { + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); + when(this.encoder.matches(any(), any())).thenReturn(true); + this.manager.setScheduler(this.scheduler); + this.manager.setPasswordEncoder(this.encoder); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + this.user, this.user.getPassword()); + + Authentication result = this.manager.authenticate(token).block(); + + verify(this.scheduler).schedule(any()); + } + + @Test + public void authenticateWhenPasswordServiceThenUpdated() { + String encodedPassword = "encoded"; + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); + when(this.encoder.matches(any(), any())).thenReturn(true); + when(this.encoder.upgradeEncoding(any())).thenReturn(true); + when(this.encoder.encode(any())).thenReturn(encodedPassword); + when(this.userDetailsPasswordService.updatePassword(any(), any())).thenReturn(Mono.just(this.user)); + this.manager.setPasswordEncoder(this.encoder); + this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + this.user, this.user.getPassword()); + + Authentication result = this.manager.authenticate(token).block(); + + verify(this.encoder).encode(this.user.getPassword()); + verify(this.userDetailsPasswordService).updatePassword(eq(this.user), eq(encodedPassword)); + } + + @Test + public void authenticateWhenPasswordServiceAndBadCredentialsThenNotUpdated() { + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); + when(this.encoder.matches(any(), any())).thenReturn(false); + this.manager.setPasswordEncoder(this.encoder); + this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + this.user, this.user.getPassword()); + + assertThatThrownBy(() -> this.manager.authenticate(token).block()) + .isInstanceOf(BadCredentialsException.class); + + verifyZeroInteractions(this.userDetailsPasswordService); + } + + @Test + public void authenticateWhenPasswordServiceAndUpgradeFalseThenNotUpdated() { + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); + when(this.encoder.matches(any(), any())).thenReturn(true); + when(this.encoder.upgradeEncoding(any())).thenReturn(false); + this.manager.setPasswordEncoder(this.encoder); + this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + this.user, this.user.getPassword()); + + Authentication result = this.manager.authenticate(token).block(); + + verifyZeroInteractions(this.userDetailsPasswordService); + } +} diff --git a/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java index 094c79ebee9..5f7c6e0a8d4 100644 --- a/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java @@ -17,12 +17,16 @@ package org.springframework.security.authentication.dao; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import java.security.SecureRandom; @@ -43,6 +47,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -53,6 +58,7 @@ import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; /** * Tests {@link DaoAuthenticationProvider}. @@ -399,6 +405,80 @@ public void testAuthenticatesWithForcePrincipalAsString() { assertThat(castResult.getPrincipal()).isEqualTo("rod"); } + @Test + public void authenticateWhenSuccessAndPasswordManagerThenUpdates() { + String password = "password"; + String encodedPassword = "encoded"; + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + "user", password); + + PasswordEncoder encoder = mock(PasswordEncoder.class); + UserDetailsService userDetailsService = mock(UserDetailsService.class); + UserDetailsPasswordService passwordManager = mock(UserDetailsPasswordService.class); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(encoder); + provider.setUserDetailsService(userDetailsService); + provider.setUserDetailsPasswordService(passwordManager); + + UserDetails user = PasswordEncodedUser.user(); + when(encoder.matches(any(), any())).thenReturn(true); + when(encoder.upgradeEncoding(any())).thenReturn(true); + when(encoder.encode(any())).thenReturn(encodedPassword); + when(userDetailsService.loadUserByUsername(any())).thenReturn(user); + when(passwordManager.updatePassword(any(), any())).thenReturn(user); + + Authentication result = provider.authenticate(token); + + verify(encoder).encode(password); + verify(passwordManager).updatePassword(eq(user), eq(encodedPassword)); + } + + @Test + public void authenticateWhenBadCredentialsAndPasswordManagerThenNoUpdate() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + "user", "password"); + + PasswordEncoder encoder = mock(PasswordEncoder.class); + UserDetailsService userDetailsService = mock(UserDetailsService.class); + UserDetailsPasswordService passwordManager = mock(UserDetailsPasswordService.class); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(encoder); + provider.setUserDetailsService(userDetailsService); + provider.setUserDetailsPasswordService(passwordManager); + + UserDetails user = PasswordEncodedUser.user(); + when(encoder.matches(any(), any())).thenReturn(false); + when(userDetailsService.loadUserByUsername(any())).thenReturn(user); + + assertThatThrownBy(() -> provider.authenticate(token)) + .isInstanceOf(BadCredentialsException.class); + + verifyZeroInteractions(passwordManager); + } + + @Test + public void authenticateWhenNotUpgradeAndPasswordManagerThenNoUpdate() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + "user", "password"); + + PasswordEncoder encoder = mock(PasswordEncoder.class); + UserDetailsService userDetailsService = mock(UserDetailsService.class); + UserDetailsPasswordService passwordManager = mock(UserDetailsPasswordService.class); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(encoder); + provider.setUserDetailsService(userDetailsService); + provider.setUserDetailsPasswordService(passwordManager); + + UserDetails user = PasswordEncodedUser.user(); + when(encoder.matches(any(), any())).thenReturn(true); + when(encoder.upgradeEncoding(any())).thenReturn(false); + when(userDetailsService.loadUserByUsername(any())).thenReturn(user); + + Authentication result = provider.authenticate(token); + + verifyZeroInteractions(passwordManager); + } + @Test public void testDetectsNullBeingReturnedFromAuthenticationDao() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/SecurityContextLoginModuleTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/SecurityContextLoginModuleTests.java index 9cd71b9923a..53beac791ac 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/SecurityContextLoginModuleTests.java +++ b/core/src/test/java/org/springframework/security/authentication/jaas/SecurityContextLoginModuleTests.java @@ -103,7 +103,7 @@ public void testLogout() throws Exception { this.module.login(); assertThat(this.module.logout()).as("Should return true as it succeeds").isTrue(); assertThat(this.module.getAuthentication()).as("Authentication should be null") - .isEqualTo(null); + .isNull(); assertThat(this.subject.getPrincipals().contains(this.auth)) .withFailMessage( diff --git a/core/src/test/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsServiceTests.java b/core/src/test/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsServiceTests.java index 5c8b701507f..186c522beac 100644 --- a/core/src/test/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsServiceTests.java +++ b/core/src/test/java/org/springframework/security/core/userdetails/MapReactiveUserDetailsServiceTests.java @@ -71,4 +71,10 @@ public void findByUsernameWhenClearCredentialsThenFindByUsernameStillHasCredenti public void findByUsernameWhenNotFoundThenEmpty() { assertThat((users.findByUsername("notfound"))).isEqualTo(Mono.empty()); } + + @Test + public void updatePassword() { + users.updatePassword(USER_DETAILS, "new").block(); + assertThat(users.findByUsername(USER_DETAILS.getUsername()).block().getPassword()).isEqualTo("new"); + } } diff --git a/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java b/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java new file mode 100644 index 00000000000..9f58cc5385f --- /dev/null +++ b/core/src/test/java/org/springframework/security/provisioning/InMemoryUserDetailsManagerTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.provisioning; + +import org.junit.Test; +import org.springframework.security.core.userdetails.PasswordEncodedUser; +import org.springframework.security.core.userdetails.UserDetails; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class InMemoryUserDetailsManagerTests { + private final UserDetails user = PasswordEncodedUser.user(); + + private InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(this.user); + + @Test + public void changePassword() { + String newPassword = "newPassword"; + this.manager.updatePassword(this.user, newPassword); + assertThat(this.manager.loadUserByUsername(this.user.getUsername()).getPassword()).isEqualTo(newPassword); + } +} diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java index b6738db848f..8f68bb80fb4 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java @@ -216,6 +216,12 @@ private String extractId(String prefixEncodedPassword) { return prefixEncodedPassword.substring(start + 1, end); } + @Override + public boolean upgradeEncoding(String encodedPassword) { + String id = extractId(encodedPassword); + return !this.idForEncode.equalsIgnoreCase(id); + } + private String extractEncodedPassword(String prefixEncodedPassword) { int start = prefixEncodedPassword.indexOf(SUFFIX); return prefixEncodedPassword.substring(start + 1); diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java index 6babace267c..84fd719c7e0 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/LdapShaPasswordEncoder.java @@ -37,7 +37,7 @@ * * @author Luke Taylor * @deprecated Digest based password encoding is not considered secure. Instead use an - * adaptive one way funciton like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or + * adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports * password upgrades. There are no plans to remove this support. It is deprecated to indicate * that this is a legacy implementation and using it is considered insecure. diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java index c8e3fe7333c..b1571ec9713 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java @@ -71,7 +71,7 @@ * @author Rob winch * @since 5.0 * @deprecated Digest based password encoding is not considered secure. Instead use an - * adaptive one way funciton like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or + * adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports * password upgrades. There are no plans to remove this support. It is deprecated to indicate * that this is a legacy implementation and using it is considered insecure. diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java index 250bbae5aae..735c5ae0505 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/MessageDigestPasswordEncoder.java @@ -74,7 +74,7 @@ * @author Rob Winch * @since 5.0 * @deprecated Digest based password encoding is not considered secure. Instead use an - * adaptive one way funciton like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or + * adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports * password upgrades. There are no plans to remove this support. It is deprecated to indicate * that this is a legacy implementation and using it is considered insecure. diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java index 2b31acd8c42..4e77d3b19b1 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/PasswordEncoder.java @@ -42,4 +42,14 @@ public interface PasswordEncoder { */ boolean matches(CharSequence rawPassword, String encodedPassword); + /** + * Returns true if the encoded password should be encoded again for better security, + * else false. The default implementation always returns false. + * @param encodedPassword the encoded password to check + * @return true if the encoded password should be encoded again for better security, + * else false. + */ + default boolean upgradeEncoding(String encodedPassword) { + return false; + } } diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java index d5deea80047..59a88de50f5 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java @@ -198,4 +198,29 @@ public void matchesWhenNullIdThenDelegatesToInvalidId() { public void matchesWhenRawPasswordNotNullAndEncodedPasswordNullThenThrowsIllegalArgumentException() { this.passwordEncoder.matches(this.rawPassword, null); } + + @Test + public void upgradeEncodingWhenEncodedPasswordNullThenTrue() { + assertThat(this.passwordEncoder.upgradeEncoding(null)).isTrue(); + } + + @Test + public void upgradeEncodingWhenNullIdThenTrue() { + assertThat(this.passwordEncoder.upgradeEncoding(this.encodedPassword)).isTrue(); + } + + @Test + public void upgradeEncodingWhenIdInvalidFormatThenTrue() { + assertThat(this.passwordEncoder.upgradeEncoding("{bcrypt"+ this.encodedPassword)).isTrue(); + } + + @Test + public void upgradeEncodingWhenSameIdThenFalse() { + assertThat(this.passwordEncoder.upgradeEncoding(this.bcryptEncodedPassword)).isFalse(); + } + + @Test + public void upgradeEncodingWhenDifferentIdThenTrue() { + assertThat(this.passwordEncoder.upgradeEncoding(this.noopEncodedPassword)).isTrue(); + } } diff --git a/docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc b/docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc index cc6c3a2bb86..462330aa8f0 100644 --- a/docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc @@ -241,6 +241,7 @@ This allows HTTPS websites to resist impersonation by attackers using mis-issued ** `Content-Security-Policy` or `Content-Security-Policy-Report-Only` - Can be set using the <> element. https://www.w3.org/TR/CSP2/[Content Security Policy (CSP)] is a mechanism that web applications can leverage to mitigate content injection vulnerabilities, such as cross-site scripting (XSS). ** `Referrer-Policy` - Can be set using the <> element, https://www.w3.org/TR/referrer-policy/[Referrer-Policy] is a mechanism that web applications can leverage to manage the referrer field, which contains the last page the user was on. +** `Feature-Policy` - Can be set using the <> element, https://wicg.github.io/feature-policy/[Feature-Policy] is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser. [[nsa-headers-attributes]] ===== Attributes @@ -272,6 +273,7 @@ The default is false (the headers are enabled). * <> * <> * <> +* <> * <> * <> * <> @@ -459,6 +461,24 @@ Default "no-referrer". +[[nsa-feature-policy]] +==== +When enabled adds the https://wicg.github.io/feature-policy/[Feature Policy] header to the response. + +[[nsa-feature-policy-attributes]] +===== Attributes + +[[nsa-feature-policy-policy-directives]] +* **policy-directives** +The security policy directive(s) for the Feature-Policy header. + +[[nsa-feature-policy-parents]] +===== Parent Elements of + +* <> + + + [[nsa-frame-options]] ==== When enabled adds the http://tools.ietf.org/html/draft-ietf-websec-x-frame-options[X-Frame-Options header] to the response, this allows newer browsers to do some security checks and prevent http://en.wikipedia.org/wiki/Clickjacking[clickjacking] attacks. diff --git a/docs/manual/src/docs/asciidoc/_includes/authorization/expression-based.adoc b/docs/manual/src/docs/asciidoc/_includes/authorization/expression-based.adoc index 85608114696..aac5044dd1d 100644 --- a/docs/manual/src/docs/asciidoc/_includes/authorization/expression-based.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/authorization/expression-based.adoc @@ -35,7 +35,7 @@ This can be customized by modifying the `defaultRolePrefix` on `DefaultWebSecuri | Returns `true` if the current principal has the specified authority. | `hasAnyAuthority([authority1,authority2])` -| Returns `true` if the current principal has any of the supplied roles (given as a comma-separated list of strings) +| Returns `true` if the current principal has any of the supplied authorities (given as a comma-separated list of strings) | `principal` | Allows direct access to the principal object representing the current user diff --git a/docs/manual/src/docs/asciidoc/_includes/data/index.adoc b/docs/manual/src/docs/asciidoc/_includes/data/index.adoc index f0cb0d9f2fd..cd4700ec9ac 100644 --- a/docs/manual/src/docs/asciidoc/_includes/data/index.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/data/index.adoc @@ -8,7 +8,7 @@ It is not only useful but necessary to include the user in the queries to suppor [[data-configuration]] == Spring Data & Spring Security Configuration -To use this support, provide a bean of type `SecurityEvaluationContextExtension`. +To use this support, add `org.springframework.security:spring-security-data` dependency and provide a bean of type `SecurityEvaluationContextExtension`. In Java Configuration, this would look like: [source,java] diff --git a/docs/manual/src/docs/asciidoc/_includes/test/method.adoc b/docs/manual/src/docs/asciidoc/_includes/test/method.adoc index a2cc64e51d2..e1550d0b382 100644 --- a/docs/manual/src/docs/asciidoc/_includes/test/method.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/test/method.adoc @@ -44,6 +44,7 @@ This is a basic example of how to setup Spring Security Test. The highlights are NOTE: Spring Security hooks into Spring Test support using the `WithSecurityContextTestExecutionListener` which will ensure our tests are ran with the correct user. It does this by populating the `SecurityContextHolder` prior to running our tests. +If you are using reactive method security, you will also need `ReactorContextTestExecutionListener` which populates `ReactiveSecurityContextHolder`. After the test is done, it will clear out the `SecurityContextHolder`. If you only need Spring Security related support, you can replace `@ContextConfiguration` with `@SecurityTestExecutionListeners`. diff --git a/docs/manual/src/docs/asciidoc/_includes/web/headers.adoc b/docs/manual/src/docs/asciidoc/_includes/web/headers.adoc index fd5bb146686..3873dc2df75 100644 --- a/docs/manual/src/docs/asciidoc/_includes/web/headers.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/web/headers.adoc @@ -714,6 +714,56 @@ protected void configure(HttpSecurity http) throws Exception { ---- +[[headers-feature]] +==== Feature Policy + +https://wicg.github.io/feature-policy/[Feature Policy] is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser. + +[source] +---- +Feature-Policy: geolocation 'self' +---- + +With Feature Policy, developers can opt-in to a set of "policies" for the browser to enforce on specific features used throughout your site. +These policies restrict what APIs the site can access or modify the browser's default behavior for certain features. + +[[headers-feature-configure]] +===== Configuring Feature Policy + +Spring Security *_doesn't add_* Feature Policy header by default. + +You can enable the Feature-Policy header using XML configuration with the <>> element as shown below: + +[source,xml] +---- + + + + + + + +---- + +Similarly, you can enable the Feature Policy header using Java configuration as shown below: + +[source,java] +---- +@EnableWebSecurity +public class WebSecurityConfig extends +WebSecurityConfigurerAdapter { + +@Override +protected void configure(HttpSecurity http) throws Exception { + http + // ... + .headers() + .featurePolicy("geolocation 'self'"); +} +} +---- + + [[headers-custom]] === Custom Headers Spring Security has mechanisms to make it convenient to add the more common security headers to your application. diff --git a/docs/manual/src/docs/asciidoc/_includes/web/security-filter-chain.adoc b/docs/manual/src/docs/asciidoc/_includes/web/security-filter-chain.adoc index 5c3d68c874d..4333b182634 100644 --- a/docs/manual/src/docs/asciidoc/_includes/web/security-filter-chain.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/web/security-filter-chain.adoc @@ -179,6 +179,45 @@ public StrictHttpFirewall httpFirewall() { } ---- +The `StrictHttpFirewall` provides a whitelist of valid HTTP methods that are allowed to protect against https://www.owasp.org/index.php/Cross_Site_Tracing[Cross Site Tracing (XST)] and https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)[HTTP Verb Tampering]. +The default valid methods are "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", and "PUT". +If your application needs to modify the valid methods, you can configure a custom `StrictHttpFirewall` bean. +For example, the following will only allow HTTP "GET" and "POST" methods: + + +[source,xml] +---- + + + +---- + +The same thing can be achieved with Java Configuration by exposing a `StrictHttpFirewall` bean. + +[source,java] +---- +@Bean +public StrictHttpFirewall httpFirewall() { + StrictHttpFirewall firewall = new StrictHttpFirewall(); + firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST")); + return firewall; +} +---- + +[TIP] +==== +If you are using `new MockHttpServletRequest()` it currently creates an HTTP method as an empty String "". +This is an invalid HTTP method and will be rejected by Spring Security. +You can resolve this by replacing it with `new MockHttpServletRequest("GET", "")`. +See https://jira.spring.io/browse/SPR-16851[SPR_16851] for an issue requesting to improve this. +==== + +If you must allow any HTTP method (not recommended), you can use `StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true)`. +This will disable validation of the HTTP method entirely. + + === Use with other Filter-Based Frameworks If you're using some other framework that is also filter-based, then you need to make sure that the Spring Security filters come first. This enables the `SecurityContextHolder` to be populated in time for use by the other filters. diff --git a/docs/manual/src/docs/asciidoc/_includes/web/websocket.adoc b/docs/manual/src/docs/asciidoc/_includes/web/websocket.adoc index 2d1d603d5ea..d01a38fe228 100644 --- a/docs/manual/src/docs/asciidoc/_includes/web/websocket.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/web/websocket.adoc @@ -4,7 +4,7 @@ Spring Security 4 added support for securing http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html[Spring's WebSocket support]. This section describes how to use Spring Security's WebSocket support. -NOTE: You can find a complete working sample of WebSocket security in samples/javaconfig/chat. +NOTE: You can find a complete working sample of WebSocket security at https://github.com/spring-projects/spring-session/tree/master/samples/boot/websocket. .Direct JSR-356 Support **** diff --git a/gradle.properties b/gradle.properties index f92573b2f0c..93f1db4d3c4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -gaeVersion=1.9.63 -springBootVersion=2.0.1.RELEASE +gaeVersion=1.9.64 +springBootVersion=2.1.0.M1 version=5.1.0.BUILD-SNAPSHOT diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index dcad76a6fa7..178f7f7f2cd 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -1,13 +1,13 @@ if (!project.hasProperty('reactorVersion')) { - ext.reactorVersion = 'Bismuth-SR9' + ext.reactorVersion = 'Californium-M1' } if (!project.hasProperty('springVersion')) { - ext.springVersion = '5.0.6.RELEASE' + ext.springVersion = '5.1.0.RC1' } if (!project.hasProperty('springDataVersion')) { - ext.springDataVersion = 'Kay-SR7' + ext.springDataVersion = 'Lovelace-RC1' } dependencyManagement { @@ -17,7 +17,7 @@ dependencyManagement { mavenBom "org.springframework.data:spring-data-releasetrain:${springDataVersion}" } dependencies { - dependency 'cglib:cglib-nodep:3.2.6' + dependency 'cglib:cglib-nodep:3.2.7' dependency 'com.squareup.okhttp3:mockwebserver:3.10.0' dependency 'opensymphony:sitemesh:2.4.2' dependency 'org.gebish:geb-spock:0.10.0' @@ -40,27 +40,27 @@ dependencyManagement { dependency 'asm:asm:3.1' dependency 'ch.qos.logback:logback-classic:1.2.3' dependency 'ch.qos.logback:logback-core:1.2.3' - dependency 'com.fasterxml.jackson.core:jackson-annotations:2.9.5' - dependency 'com.fasterxml.jackson.core:jackson-core:2.9.5' - dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.5' + dependency 'com.fasterxml.jackson.core:jackson-annotations:2.9.6' + dependency 'com.fasterxml.jackson.core:jackson-core:2.9.6' + dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.6' dependency 'com.fasterxml:classmate:1.3.4' dependency 'com.github.stephenc.jcip:jcip-annotations:1.0-1' - dependency 'com.google.appengine:appengine-api-1.0-sdk:1.9.63' - dependency 'com.google.appengine:appengine-api-labs:1.9.63' - dependency 'com.google.appengine:appengine-api-stubs:1.9.63' - dependency 'com.google.appengine:appengine-testing:1.9.63' + dependency 'com.google.appengine:appengine-api-1.0-sdk:1.9.64' + dependency 'com.google.appengine:appengine-api-labs:1.9.64' + dependency 'com.google.appengine:appengine-api-stubs:1.9.64' + dependency 'com.google.appengine:appengine-testing:1.9.64' dependency 'com.google.appengine:appengine:1.9.63' dependency 'com.google.code.gson:gson:2.8.2' dependency 'com.google.guava:guava:20.0' dependency 'com.google.inject:guice:3.0' dependency 'com.nimbusds:lang-tag:1.4.3' - dependency 'com.nimbusds:nimbus-jose-jwt:5.10' - dependency 'com.nimbusds:oauth2-oidc-sdk:5.61' + dependency 'com.nimbusds:nimbus-jose-jwt:5.14' + dependency 'com.nimbusds:oauth2-oidc-sdk:5.64.2' dependency 'com.squareup.okhttp3:okhttp:3.9.0' dependency 'com.squareup.okio:okio:1.13.0' dependency 'com.sun.xml.bind:jaxb-core:2.3.0' dependency 'com.sun.xml.bind:jaxb-impl:2.3.0' - dependency 'com.unboundid:unboundid-ldapsdk:4.0.5' + dependency 'com.unboundid:unboundid-ldapsdk:4.0.6' dependency 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1' dependency 'commons-cli:commons-cli:1.4' dependency 'commons-codec:commons-codec:1.11' @@ -74,7 +74,7 @@ dependencyManagement { dependency 'javax.annotation:jsr250-api:1.0' dependency 'javax.inject:javax.inject:1' dependency 'javax.mail:mail:1.4.7' - dependency 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.1' + dependency 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2' dependency 'javax.servlet.jsp:javax.servlet.jsp-api:2.3.2-b02' dependency 'javax.servlet:javax.servlet-api:4.0.1' dependency 'javax.validation:validation-api:2.0.1.Final' @@ -86,11 +86,9 @@ dependencyManagement { dependency 'net.jcip:jcip-annotations:1.0' dependency 'net.minidev:accessors-smart:1.2' dependency 'net.minidev:json-smart:2.3' - dependency 'net.sf.ehcache:ehcache:2.10.4' - dependency 'net.sourceforge.cssparser:cssparser:0.9.24' - dependency 'net.sourceforge.htmlunit:htmlunit-core-js:2.28' - dependency 'net.sourceforge.htmlunit:htmlunit:2.30' - dependency 'net.sourceforge.htmlunit:neko-htmlunit:2.28' + dependency 'net.sf.ehcache:ehcache:2.10.5' + dependency 'net.sourceforge.htmlunit:htmlunit:2.31' + dependency 'net.sourceforge.htmlunit:neko-htmlunit:2.31' dependency 'net.sourceforge.nekohtml:nekohtml:1.9.22' dependency 'nz.net.ultraq.thymeleaf:thymeleaf-expression-processor:1.1.3' dependency 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:2.3.0' @@ -129,7 +127,7 @@ dependencyManagement { dependency 'org.apache.directory.shared:shared-cursor:0.9.15' dependency 'org.apache.directory.shared:shared-ldap-constants:0.9.15' dependency 'org.apache.directory.shared:shared-ldap:0.9.15' - dependency 'org.apache.httpcomponents:httpclient:4.5.5' + dependency 'org.apache.httpcomponents:httpclient:4.5.6' dependency 'org.apache.httpcomponents:httpcore:4.4.8' dependency 'org.apache.httpcomponents:httpmime:4.5.3' dependency 'org.apache.mina:mina-core:2.0.0-M6' @@ -145,9 +143,9 @@ dependencyManagement { dependency 'org.aspectj:aspectjrt:1.9.1' dependency 'org.aspectj:aspectjtools:1.9.1' dependency 'org.aspectj:aspectjweaver:1.9.1' - dependency 'org.assertj:assertj-core:3.9.1' + dependency 'org.assertj:assertj-core:3.10.0' dependency 'org.attoparser:attoparser:2.0.4.RELEASE' - dependency 'org.bouncycastle:bcpkix-jdk15on:1.59' + dependency 'org.bouncycastle:bcpkix-jdk15on:1.60' dependency 'org.bouncycastle:bcprov-jdk15on:1.58' dependency 'org.codehaus.groovy:groovy-all:2.4.14' dependency 'org.codehaus.groovy:groovy-json:2.4.14' @@ -172,10 +170,10 @@ dependencyManagement { dependency 'org.hamcrest:hamcrest-core:1.3' dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final' dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final' - dependency 'org.hibernate:hibernate-core:5.2.12.Final' - dependency 'org.hibernate:hibernate-entitymanager:5.2.16.Final' - dependency 'org.hibernate:hibernate-validator:6.0.9.Final' - dependency 'org.hsqldb:hsqldb:2.4.0' + dependency 'org.hibernate:hibernate-core:5.2.17.Final' + dependency 'org.hibernate:hibernate-entitymanager:5.3.3.Final' + dependency 'org.hibernate:hibernate-validator:6.0.11.Final' + dependency 'org.hsqldb:hsqldb:2.4.1' dependency 'org.jasig.cas.client:cas-client-core:3.5.0' dependency 'org.javassist:javassist:3.22.0-CR2' dependency 'org.jboss.logging:jboss-logging:3.3.1.Final' @@ -186,10 +184,9 @@ dependencyManagement { dependency 'org.openid4java:openid4java-nodeps:0.9.6' dependency 'org.ow2.asm:asm:6.0' dependency 'org.reactivestreams:reactive-streams:1.0.1' - dependency 'org.seleniumhq.selenium:htmlunit-driver:2.30.0' - dependency 'org.seleniumhq.selenium:selenium-api:3.8.1' - dependency 'org.seleniumhq.selenium:selenium-java:3.11.0' - dependency 'org.seleniumhq.selenium:selenium-support:3.11.0' + dependency 'org.seleniumhq.selenium:htmlunit-driver:2.31.0' + dependency 'org.seleniumhq.selenium:selenium-java:3.13.0' + dependency 'org.seleniumhq.selenium:selenium-support:3.13.0' dependency 'org.skyscreamer:jsonassert:1.5.0' dependency 'org.slf4j:jcl-over-slf4j:1.7.25' dependency 'org.slf4j:jul-to-slf4j:1.7.25' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a5fe1cb94b9..91ca28c8b80 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ea720f986f3..d2c45a4b260 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle index 94f29ae6074..188582aa640 100644 --- a/oauth2/oauth2-client/spring-security-oauth2-client.gradle +++ b/oauth2/oauth2-client/spring-security-oauth2-client.gradle @@ -14,7 +14,7 @@ dependencies { testCompile powerMock2Dependencies testCompile 'com.squareup.okhttp3:mockwebserver' testCompile 'com.fasterxml.jackson.core:jackson-databind' - testCompile 'io.projectreactor.ipc:reactor-netty' + testCompile 'io.projectreactor.netty:reactor-netty' testCompile 'io.projectreactor:reactor-test' provided 'javax.servlet:javax.servlet-api' diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.java index 392a947705a..298658324a0 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -15,8 +15,10 @@ */ package org.springframework.security.oauth2.client; +import org.springframework.lang.Nullable; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.util.Assert; /** @@ -33,11 +35,13 @@ * @since 5.0 * @see ClientRegistration * @see OAuth2AccessToken + * @see OAuth2RefreshToken */ public class OAuth2AuthorizedClient { private final ClientRegistration clientRegistration; private final String principalName; private final OAuth2AccessToken accessToken; + private final OAuth2RefreshToken refreshToken; /** * Constructs an {@code OAuth2AuthorizedClient} using the provided parameters. @@ -47,12 +51,26 @@ public class OAuth2AuthorizedClient { * @param accessToken the access token credential granted */ public OAuth2AuthorizedClient(ClientRegistration clientRegistration, String principalName, OAuth2AccessToken accessToken) { + this(clientRegistration, principalName, accessToken, null); + } + + /** + * Constructs an {@code OAuth2AuthorizedClient} using the provided parameters. + * + * @param clientRegistration the authorized client's registration + * @param principalName the name of the End-User {@code Principal} (Resource Owner) + * @param accessToken the access token credential granted + * @param refreshToken the refresh token credential granted + */ + public OAuth2AuthorizedClient(ClientRegistration clientRegistration, String principalName, + OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) { Assert.notNull(clientRegistration, "clientRegistration cannot be null"); Assert.hasText(principalName, "principalName cannot be empty"); Assert.notNull(accessToken, "accessToken cannot be null"); this.clientRegistration = clientRegistration; this.principalName = principalName; this.accessToken = accessToken; + this.refreshToken = refreshToken; } /** @@ -81,4 +99,14 @@ public String getPrincipalName() { public OAuth2AccessToken getAccessToken() { return this.accessToken; } + + /** + * Returns the {@link OAuth2RefreshToken refresh token} credential granted. + * + * @since 5.1 + * @return the {@link OAuth2RefreshToken} + */ + public @Nullable OAuth2RefreshToken getRefreshToken() { + return this.refreshToken; + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/OAuth2Client.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/RegisteredOAuth2AuthorizedClient.java similarity index 62% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/OAuth2Client.java rename to oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/RegisteredOAuth2AuthorizedClient.java index c8bd1c7af66..7bc975fab16 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/OAuth2Client.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/RegisteredOAuth2AuthorizedClient.java @@ -17,9 +17,7 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.web.method.annotation.OAuth2ClientArgumentResolver; -import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.client.web.method.annotation.OAuth2AuthorizedClientArgumentResolver; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -28,40 +26,29 @@ import java.lang.annotation.Target; /** - * This annotation may be used to resolve a method parameter into an argument value - * for the following types: {@link ClientRegistration}, {@link OAuth2AuthorizedClient} - * and {@link OAuth2AccessToken}. + * This annotation may be used to resolve a method parameter + * to an argument value of type {@link OAuth2AuthorizedClient}. * *

        * For example: *

          * @Controller
          * public class MyController {
        - *     @GetMapping("/client-registration")
        - *     public String clientRegistration(@OAuth2Client("login-client") ClientRegistration clientRegistration) {
        - *         // do something with clientRegistration
        - *     }
        - *
          *     @GetMapping("/authorized-client")
        - *     public String authorizedClient(@OAuth2Client("login-client") OAuth2AuthorizedClient authorizedClient) {
        + *     public String authorizedClient(@RegisteredOAuth2AuthorizedClient("login-client") OAuth2AuthorizedClient authorizedClient) {
          *         // do something with authorizedClient
          *     }
        - *
        - *     @GetMapping("/access-token")
        - *     public String accessToken(@OAuth2Client("login-client") OAuth2AccessToken accessToken) {
        - *         // do something with accessToken
        - *     }
          * }
          * 
        * * @author Joe Grandja * @since 5.1 - * @see OAuth2ClientArgumentResolver + * @see OAuth2AuthorizedClientArgumentResolver */ @Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented -public @interface OAuth2Client { +public @interface RegisteredOAuth2AuthorizedClient { /** * Sets the client registration identifier. @@ -74,8 +61,8 @@ /** * The default attribute for this annotation. * This is an alias for {@link #registrationId()}. - * For example, {@code @OAuth2Client("login-client")} is equivalent to - * {@code @OAuth2Client(registrationId="login-client")}. + * For example, {@code @RegisteredOAuth2AuthorizedClient("login-client")} is equivalent to + * {@code @RegisteredOAuth2AuthorizedClient(registrationId="login-client")}. * * @return the client registration identifier */ diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java index c202a31f3e7..e88fc1e43ea 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -20,7 +20,6 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; -import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.util.Assert; @@ -69,13 +68,12 @@ public Authentication authenticate(Authentication authentication) throws Authent authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange())); - OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); - OAuth2AuthorizationCodeAuthenticationToken authenticationResult = new OAuth2AuthorizationCodeAuthenticationToken( authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange(), - accessToken); + accessTokenResponse.getAccessToken(), + accessTokenResponse.getRefreshToken()); authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); return authenticationResult; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java index bbc6d4aca1a..969a10a0a82 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java @@ -15,10 +15,12 @@ */ package org.springframework.security.oauth2.client.authentication; +import org.springframework.lang.Nullable; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.util.Assert; @@ -40,6 +42,7 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti private ClientRegistration clientRegistration; private OAuth2AuthorizationExchange authorizationExchange; private OAuth2AccessToken accessToken; + private OAuth2RefreshToken refreshToken; /** * This constructor should be used when the Authorization Request/Response is complete. @@ -67,9 +70,26 @@ public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegis public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration, OAuth2AuthorizationExchange authorizationExchange, OAuth2AccessToken accessToken) { + this(clientRegistration, authorizationExchange, accessToken, null); + } + + /** + * This constructor should be used when the Access Token Request/Response is complete, + * which indicates that the Authorization Code Grant flow has fully completed. + * + * @param clientRegistration the client registration + * @param authorizationExchange the authorization exchange + * @param accessToken the access token credential + * @param refreshToken the refresh token credential + */ + public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange, + OAuth2AccessToken accessToken, + @Nullable OAuth2RefreshToken refreshToken) { this(clientRegistration, authorizationExchange); Assert.notNull(accessToken, "accessToken cannot be null"); this.accessToken = accessToken; + this.refreshToken = refreshToken; this.setAuthenticated(true); } @@ -111,4 +131,13 @@ public OAuth2AuthorizationExchange getAuthorizationExchange() { public OAuth2AccessToken getAccessToken() { return this.accessToken; } + + /** + * Returns the {@link OAuth2RefreshToken refresh token}. + * + * @return the {@link OAuth2RefreshToken} + */ + public @Nullable OAuth2RefreshToken getRefreshToken() { + return this.refreshToken; + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java index d3c442f334b..6c032fb073a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -30,6 +30,7 @@ import org.springframework.util.Assert; import java.util.Collection; +import java.util.Map; /** * An implementation of an {@link AuthenticationProvider} for OAuth 2.0 Login, @@ -101,9 +102,10 @@ public Authentication authenticate(Authentication authentication) throws Authent authorizationCodeAuthentication.getAuthorizationExchange())); OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); + Map additionalParameters = accessTokenResponse.getAdditionalParameters(); - OAuth2User oauth2User = this.userService.loadUser( - new OAuth2UserRequest(authorizationCodeAuthentication.getClientRegistration(), accessToken)); + OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest( + authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters)); Collection mappedAuthorities = this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities()); @@ -113,7 +115,8 @@ public Authentication authenticate(Authentication authentication) throws Authent authorizationCodeAuthentication.getAuthorizationExchange(), oauth2User, mappedAuthorities, - accessToken); + accessToken, + accessTokenResponse.getRefreshToken()); authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); return authenticationResult; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java index 0709ac7cd1a..32311da81e4 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -15,11 +15,13 @@ */ package org.springframework.security.oauth2.client.authentication; +import org.springframework.lang.Nullable; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.util.Assert; @@ -46,6 +48,7 @@ public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken private ClientRegistration clientRegistration; private OAuth2AuthorizationExchange authorizationExchange; private OAuth2AccessToken accessToken; + private OAuth2RefreshToken refreshToken; /** * This constructor should be used when the Authorization Request/Response is complete. @@ -80,6 +83,27 @@ public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration, OAuth2User principal, Collection authorities, OAuth2AccessToken accessToken) { + this(clientRegistration, authorizationExchange, principal, authorities, accessToken, null); + } + + /** + * This constructor should be used when the Access Token Request/Response is complete, + * which indicates that the Authorization Code Grant flow has fully completed + * and OAuth 2.0 Login has been achieved. + * + * @param clientRegistration the client registration + * @param authorizationExchange the authorization exchange + * @param principal the user {@code Principal} registered with the OAuth 2.0 Provider + * @param authorities the authorities granted to the user + * @param accessToken the access token credential + * @param refreshToken the refresh token credential + */ + public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange, + OAuth2User principal, + Collection authorities, + OAuth2AccessToken accessToken, + @Nullable OAuth2RefreshToken refreshToken) { super(authorities); Assert.notNull(clientRegistration, "clientRegistration cannot be null"); Assert.notNull(authorizationExchange, "authorizationExchange cannot be null"); @@ -89,6 +113,7 @@ public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration, this.authorizationExchange = authorizationExchange; this.principal = principal; this.accessToken = accessToken; + this.refreshToken = refreshToken; this.setAuthenticated(true); } @@ -128,4 +153,14 @@ public OAuth2AuthorizationExchange getAuthorizationExchange() { public OAuth2AccessToken getAccessToken() { return this.accessToken; } + + /** + * Returns the {@link OAuth2RefreshToken refresh token}. + * + * @since 5.1 + * @return the {@link OAuth2RefreshToken} + */ + public @Nullable OAuth2RefreshToken getRefreshToken() { + return this.refreshToken; + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java index 8509db1f2f4..eb2f161c77d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.client.authentication; import java.util.Collection; +import java.util.Map; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.core.Authentication; @@ -109,7 +110,9 @@ public Mono authenticate(Authentication authentication) { private Mono authenticationResult(OAuth2LoginAuthenticationToken authorizationCodeAuthentication, OAuth2AccessTokenResponse accessTokenResponse) { OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); - OAuth2UserRequest userRequest = new OAuth2UserRequest(authorizationCodeAuthentication.getClientRegistration(), accessToken); + Map additionalParameters = accessTokenResponse.getAdditionalParameters(); + OAuth2UserRequest userRequest = new OAuth2UserRequest( + authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters); return this.userService.loadUser(userRequest) .flatMap(oauth2User -> { Collection mappedAuthorities = @@ -120,11 +123,13 @@ private Mono authenticationResult(OAuth2LoginAuthenti authorizationCodeAuthentication.getAuthorizationExchange(), oauth2User, mappedAuthorities, - accessToken); + accessToken, + accessTokenResponse.getRefreshToken()); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( authenticationResult.getClientRegistration(), authenticationResult.getName(), - authenticationResult.getAccessToken()); + authenticationResult.getAccessToken(), + authenticationResult.getRefreshToken()); OAuth2AuthenticationToken result = new OAuth2AuthenticationToken( authenticationResult.getPrincipal(), authenticationResult.getAuthorities(), diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java new file mode 100644 index 00000000000..f99409a276c --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java @@ -0,0 +1,270 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.endpoint; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The default implementation of an {@link OAuth2AccessTokenResponseClient} + * for the {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} grant. + * This implementation uses a {@link RestOperations} when requesting + * an access token credential at the Authorization Server's Token Endpoint. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AccessTokenResponseClient + * @see OAuth2ClientCredentialsGrantRequest + * @see OAuth2AccessTokenResponse + * @see Section 4.4.2 Access Token Request (Client Credentials Grant) + * @see Section 4.4.3 Access Token Response (Client Credentials Grant) + */ +public class DefaultClientCredentialsTokenResponseClient implements OAuth2AccessTokenResponseClient { + private static final String INVALID_TOKEN_REQUEST_ERROR_CODE = "invalid_token_request"; + + private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response"; + + private static final String[] TOKEN_RESPONSE_PARAMETER_NAMES = { + OAuth2ParameterNames.ACCESS_TOKEN, + OAuth2ParameterNames.TOKEN_TYPE, + OAuth2ParameterNames.EXPIRES_IN, + OAuth2ParameterNames.SCOPE, + OAuth2ParameterNames.REFRESH_TOKEN + }; + + private RestOperations restOperations; + + public DefaultClientCredentialsTokenResponseClient() { + RestTemplate restTemplate = new RestTemplate(); + // Disable the ResponseErrorHandler as errors are handled directly within this class + restTemplate.setErrorHandler(new NoOpResponseErrorHandler()); + this.restOperations = restTemplate; + } + + @Override + public OAuth2AccessTokenResponse getTokenResponse(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) + throws OAuth2AuthenticationException { + + Assert.notNull(clientCredentialsGrantRequest, "clientCredentialsGrantRequest cannot be null"); + + // Build request + RequestEntity> request = this.buildRequest(clientCredentialsGrantRequest); + + // Exchange + ResponseEntity> response; + try { + response = this.restOperations.exchange( + request, new ParameterizedTypeReference>() {}); + } catch (Exception ex) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_REQUEST_ERROR_CODE, + "An error occurred while sending the Access Token Request: " + ex.getMessage(), null); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); + } + + Map responseParameters = response.getBody(); + + // Check for Error Response + if (response.getStatusCodeValue() != 200) { + OAuth2Error oauth2Error = this.parseErrorResponse(responseParameters); + if (oauth2Error == null) { + oauth2Error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR); + } + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + // Success Response + OAuth2AccessTokenResponse tokenResponse; + try { + tokenResponse = this.parseTokenResponse(responseParameters); + } catch (Exception ex) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, + "An error occurred parsing the Access Token response (200 OK): " + ex.getMessage(), null); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); + } + + if (tokenResponse == null) { + // This should never happen as long as the provider + // implements a Successful Response as defined in Section 5.1 + // https://tools.ietf.org/html/rfc6749#section-5.1 + OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, + "An error occurred parsing the Access Token response (200 OK). " + + "Missing required parameters: access_token and/or token_type", null); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) { + // As per spec, in Section 5.1 Successful Access Token Response + // https://tools.ietf.org/html/rfc6749#section-5.1 + // If AccessTokenResponse.scope is empty, then default to the scope + // originally requested by the client in the Token Request + tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) + .scopes(clientCredentialsGrantRequest.getClientRegistration().getScopes()) + .build(); + } + + return tokenResponse; + } + + private RequestEntity> buildRequest(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) { + HttpHeaders headers = this.buildHeaders(clientCredentialsGrantRequest); + MultiValueMap formParameters = this.buildFormParameters(clientCredentialsGrantRequest); + URI uri = UriComponentsBuilder.fromUriString(clientCredentialsGrantRequest.getClientRegistration().getProviderDetails().getTokenUri()) + .build() + .toUri(); + + return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri); + } + + private HttpHeaders buildHeaders(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) { + ClientRegistration clientRegistration = clientCredentialsGrantRequest.getClientRegistration(); + + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) { + headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret()); + } + + return headers; + } + + private MultiValueMap buildFormParameters(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) { + ClientRegistration clientRegistration = clientCredentialsGrantRequest.getClientRegistration(); + + MultiValueMap formParameters = new LinkedMultiValueMap<>(); + formParameters.add(OAuth2ParameterNames.GRANT_TYPE, clientCredentialsGrantRequest.getGrantType().getValue()); + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + formParameters.add(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " ")); + } + if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) { + formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId()); + formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); + } + + return formParameters; + } + + private OAuth2Error parseErrorResponse(Map responseParameters) { + if (CollectionUtils.isEmpty(responseParameters) || + !responseParameters.containsKey(OAuth2ParameterNames.ERROR)) { + return null; + } + + String errorCode = responseParameters.get(OAuth2ParameterNames.ERROR); + String errorDescription = responseParameters.get(OAuth2ParameterNames.ERROR_DESCRIPTION); + String errorUri = responseParameters.get(OAuth2ParameterNames.ERROR_URI); + + return new OAuth2Error(errorCode, errorDescription, errorUri); + } + + private OAuth2AccessTokenResponse parseTokenResponse(Map responseParameters) { + if (CollectionUtils.isEmpty(responseParameters) || + !responseParameters.containsKey(OAuth2ParameterNames.ACCESS_TOKEN) || + !responseParameters.containsKey(OAuth2ParameterNames.TOKEN_TYPE)) { + return null; + } + + String accessToken = responseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN); + + OAuth2AccessToken.TokenType accessTokenType = null; + if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase( + responseParameters.get(OAuth2ParameterNames.TOKEN_TYPE))) { + accessTokenType = OAuth2AccessToken.TokenType.BEARER; + } + + long expiresIn = 0; + if (responseParameters.containsKey(OAuth2ParameterNames.EXPIRES_IN)) { + try { + expiresIn = Long.valueOf(responseParameters.get(OAuth2ParameterNames.EXPIRES_IN)); + } catch (NumberFormatException ex) { } + } + + Set scopes = Collections.emptySet(); + if (responseParameters.containsKey(OAuth2ParameterNames.SCOPE)) { + String scope = responseParameters.get(OAuth2ParameterNames.SCOPE); + scopes = Arrays.stream(StringUtils.delimitedListToStringArray(scope, " ")).collect(Collectors.toSet()); + } + + Map additionalParameters = new LinkedHashMap<>(); + Set tokenResponseParameterNames = Stream.of(TOKEN_RESPONSE_PARAMETER_NAMES).collect(Collectors.toSet()); + responseParameters.entrySet().stream() + .filter(e -> !tokenResponseParameterNames.contains(e.getKey())) + .forEach(e -> additionalParameters.put(e.getKey(), e.getValue())); + + return OAuth2AccessTokenResponse.withToken(accessToken) + .tokenType(accessTokenType) + .expiresIn(expiresIn) + .scopes(scopes) + .additionalParameters(additionalParameters) + .build(); + } + + /** + * Sets the {@link RestOperations} used when requesting the access token response. + * + * @param restOperations the {@link RestOperations} used when requesting the access token response + */ + public final void setRestOperations(RestOperations restOperations) { + Assert.notNull(restOperations, "restOperations cannot be null"); + this.restOperations = restOperations; + } + + private static class NoOpResponseErrorHandler implements ResponseErrorHandler { + + @Override + public boolean hasError(ClientHttpResponse response) throws IOException { + return false; + } + + @Override + public void handleError(ClientHttpResponse response) throws IOException { + } + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClient.java index ba791b7316a..7957a2dd1b1 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -37,6 +37,7 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.util.CollectionUtils; @@ -111,8 +112,15 @@ public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRe if (!tokenResponse.indicatesSuccess()) { TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse; ErrorObject errorObject = tokenErrorResponse.getErrorObject(); - OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), errorObject.getDescription(), - (errorObject.getURI() != null ? errorObject.getURI().toString() : null)); + OAuth2Error oauth2Error; + if (errorObject == null) { + oauth2Error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR); + } else { + oauth2Error = new OAuth2Error( + errorObject.getCode() != null ? errorObject.getCode() : OAuth2ErrorCodes.SERVER_ERROR, + errorObject.getDescription(), + errorObject.getURI() != null ? errorObject.getURI().toString() : null); + } throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } @@ -138,12 +146,18 @@ public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRe accessTokenResponse.getTokens().getAccessToken().getScope().toStringList()); } + String refreshToken = null; + if (accessTokenResponse.getTokens().getRefreshToken() != null) { + refreshToken = accessTokenResponse.getTokens().getRefreshToken().getValue(); + } + Map additionalParameters = new LinkedHashMap<>(accessTokenResponse.getCustomParameters()); return OAuth2AccessTokenResponse.withToken(accessToken) .tokenType(accessTokenType) .expiresIn(expiresIn) .scopes(scopes) + .refreshToken(refreshToken) .additionalParameters(additionalParameters) .build(); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java deleted file mode 100644 index cb937ea12db..00000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClient.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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.endpoint; - -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; - -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.MediaType; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; -import org.springframework.util.CollectionUtils; -import org.springframework.web.reactive.function.BodyInserters; -import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; -import org.springframework.web.reactive.function.client.WebClient; - -import com.nimbusds.oauth2.sdk.AccessTokenResponse; -import com.nimbusds.oauth2.sdk.ErrorObject; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.TokenErrorResponse; -import com.nimbusds.oauth2.sdk.TokenResponse; -import com.nimbusds.oauth2.sdk.token.AccessToken; - -import net.minidev.json.JSONObject; -import reactor.core.publisher.Mono; - -/** - * An implementation of an {@link ReactiveOAuth2AccessTokenResponseClient} that "exchanges" - * an authorization code credential for an access token credential - * at the Authorization Server's Token Endpoint. - * - *

        - * NOTE: This implementation uses the Nimbus OAuth 2.0 SDK internally. - * - * @author Rob Winch - * @since 5.1 - * @see OAuth2AccessTokenResponseClient - * @see OAuth2AuthorizationCodeGrantRequest - * @see OAuth2AccessTokenResponse - * @see Nimbus OAuth 2.0 SDK - * @see Section 4.1.3 Access Token Request (Authorization Code Grant) - * @see Section 4.1.4 Access Token Response (Authorization Code Grant) - */ -public class NimbusReactiveAuthorizationCodeTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient { - private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response"; - - private WebClient webClient = WebClient.builder() - .filter(ExchangeFilterFunctions.basicAuthentication()) - .build(); - - @Override - public Mono getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) - throws OAuth2AuthenticationException { - - return Mono.defer(() -> { - ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration(); - - OAuth2AuthorizationExchange authorizationExchange = authorizationGrantRequest.getAuthorizationExchange(); - String tokenUri = clientRegistration.getProviderDetails().getTokenUri(); - BodyInserters.FormInserter body = body(authorizationExchange); - - return this.webClient.post() - .uri(tokenUri) - .accept(MediaType.APPLICATION_JSON) - .attributes(basicAuthenticationCredentials(clientRegistration.getClientId(), clientRegistration.getClientSecret())) - .body(body) - .retrieve() - .onStatus(s -> false, response -> { - throw new IllegalStateException("Disabled Status Handlers"); - }) - .bodyToMono(new ParameterizedTypeReference>() {}) - .map(json -> parse(json)) - .flatMap(tokenResponse -> accessTokenResponse(tokenResponse)) - .map(accessTokenResponse -> { - AccessToken accessToken = accessTokenResponse.getTokens().getAccessToken(); - OAuth2AccessToken.TokenType accessTokenType = null; - if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase( - accessToken.getType().getValue())) { - accessTokenType = OAuth2AccessToken.TokenType.BEARER; - } - long expiresIn = accessToken.getLifetime(); - - // As per spec, in section 5.1 Successful Access Token Response - // https://tools.ietf.org/html/rfc6749#section-5.1 - // If AccessTokenResponse.scope is empty, then default to the scope - // originally requested by the client in the Authorization Request - Set scopes; - if (CollectionUtils.isEmpty( - accessToken.getScope())) { - scopes = new LinkedHashSet<>( - authorizationExchange.getAuthorizationRequest().getScopes()); - } - else { - scopes = new LinkedHashSet<>( - accessToken.getScope().toStringList()); - } - - Map additionalParameters = new LinkedHashMap<>( - accessTokenResponse.getCustomParameters()); - - return OAuth2AccessTokenResponse.withToken(accessToken.getValue()) - .tokenType(accessTokenType) - .expiresIn(expiresIn) - .scopes(scopes) - .additionalParameters(additionalParameters) - .build(); - }); - }); - } - - private static BodyInserters.FormInserter body(OAuth2AuthorizationExchange authorizationExchange) { - OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse(); - String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri(); - BodyInserters.FormInserter body = BodyInserters - .fromFormData("grant_type", AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) - .with("code", authorizationResponse.getCode()); - if (redirectUri != null) { - body.with("redirect_uri", redirectUri); - } - return body; - } - - private static Mono accessTokenResponse(TokenResponse tokenResponse) { - if (tokenResponse.indicatesSuccess()) { - return Mono.just(tokenResponse) - .cast(AccessTokenResponse.class); - } - TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse; - ErrorObject errorObject = tokenErrorResponse.getErrorObject(); - OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), - errorObject.getDescription(), (errorObject.getURI() != null ? - errorObject.getURI().toString() : - null)); - - return Mono.error(new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString())); - } - - private static TokenResponse parse(Map json) { - try { - return TokenResponse.parse(new JSONObject(json)); - } - catch (ParseException pe) { - OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, - "An error occurred parsing the Access Token response: " + pe.getMessage(), null); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), pe); - } - } -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequest.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequest.java new file mode 100644 index 00000000000..9f62c8671c9 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.endpoint; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; + +/** + * An OAuth 2.0 Client Credentials Grant request that holds + * the client's credentials in {@link #getClientRegistration()}. + * + * @author Joe Grandja + * @since 5.1 + * @see AbstractOAuth2AuthorizationGrantRequest + * @see ClientRegistration + * @see Section 1.3.4 Client Credentials Grant + */ +public class OAuth2ClientCredentialsGrantRequest extends AbstractOAuth2AuthorizationGrantRequest { + private final ClientRegistration clientRegistration; + + /** + * Constructs an {@code OAuth2ClientCredentialsGrantRequest} using the provided parameters. + * + * @param clientRegistration the client registration + */ + public OAuth2ClientCredentialsGrantRequest(ClientRegistration clientRegistration) { + super(AuthorizationGrantType.CLIENT_CREDENTIALS); + Assert.notNull(clientRegistration, "clientRegistration cannot be null"); + Assert.isTrue(AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType()), + "clientRegistration.authorizationGrantType must be AuthorizationGrantType.CLIENT_CREDENTIALS"); + this.clientRegistration = clientRegistration; + } + + /** + * Returns the {@link ClientRegistration client registration}. + * + * @return the {@link ClientRegistration} + */ + public ClientRegistration getClientRegistration() { + return this.clientRegistration; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java new file mode 100644 index 00000000000..6fdee0c9ce6 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClient.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.endpoint; + +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse; + +/** + * An implementation of an {@link ReactiveOAuth2AccessTokenResponseClient} that "exchanges" + * an authorization code credential for an access token credential + * at the Authorization Server's Token Endpoint. + * + *

        + * NOTE: This implementation uses the Nimbus OAuth 2.0 SDK internally. + * + * @author Rob Winch + * @since 5.1 + * @see OAuth2AccessTokenResponseClient + * @see OAuth2AuthorizationCodeGrantRequest + * @see OAuth2AccessTokenResponse + * @see Nimbus OAuth 2.0 SDK + * @see Section 4.1.3 Access Token Request (Authorization Code Grant) + * @see Section 4.1.4 Access Token Response (Authorization Code Grant) + */ +public class WebClientReactiveAuthorizationCodeTokenResponseClient implements ReactiveOAuth2AccessTokenResponseClient { + private WebClient webClient = WebClient.builder() + .build(); + + @Override + public Mono getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) + throws OAuth2AuthenticationException { + + return Mono.defer(() -> { + ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration(); + + OAuth2AuthorizationExchange authorizationExchange = authorizationGrantRequest.getAuthorizationExchange(); + String tokenUri = clientRegistration.getProviderDetails().getTokenUri(); + BodyInserters.FormInserter body = body(authorizationExchange); + + return this.webClient.post() + .uri(tokenUri) + .accept(MediaType.APPLICATION_JSON) + .headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret())) + .body(body) + .exchange() + .flatMap(response -> response.body(oauth2AccessTokenResponse())) + .map(response -> { + if (response.getAccessToken().getScopes().isEmpty()) { + response = OAuth2AccessTokenResponse.withResponse(response) + .scopes(authorizationExchange.getAuthorizationRequest().getScopes()) + .build(); + } + return response; + }); + }); + } + + private static BodyInserters.FormInserter body(OAuth2AuthorizationExchange authorizationExchange) { + OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse(); + String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri(); + BodyInserters.FormInserter body = BodyInserters + .fromFormData("grant_type", AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .with("code", authorizationResponse.getCode()); + if (redirectUri != null) { + body.with("redirect_uri", redirectUri); + } + return body; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java index 33f2be8840e..87a64227ead 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -15,6 +15,10 @@ */ package org.springframework.security.oauth2.client.oidc.authentication; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -27,7 +31,6 @@ import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; @@ -41,16 +44,8 @@ import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import java.net.URL; -import java.time.Instant; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - /** * An implementation of an {@link AuthenticationProvider} * for the OpenID Connect Core 1.0 Authorization Code Grant Flow. @@ -142,27 +137,20 @@ public Authentication authenticate(Authentication authentication) throws Authent authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange())); - OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); - ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration(); - if (!accessTokenResponse.getAdditionalParameters().containsKey(OidcParameterNames.ID_TOKEN)) { + Map additionalParameters = accessTokenResponse.getAdditionalParameters(); + if (!additionalParameters.containsKey(OidcParameterNames.ID_TOKEN)) { OAuth2Error invalidIdTokenError = new OAuth2Error( INVALID_ID_TOKEN_ERROR_CODE, "Missing (required) ID Token in Token Response for Client Registration: " + clientRegistration.getRegistrationId(), null); throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); } + OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse); - JwtDecoder jwtDecoder = this.getJwtDecoder(clientRegistration); - Jwt jwt = jwtDecoder.decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN)); - OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()); - - this.validateIdToken(idToken, clientRegistration); - - OidcUser oidcUser = this.userService.loadUser( - new OidcUserRequest(clientRegistration, accessToken, idToken)); - + OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest( + clientRegistration, accessTokenResponse.getAccessToken(), idToken, additionalParameters)); Collection mappedAuthorities = this.authoritiesMapper.mapAuthorities(oidcUser.getAuthorities()); @@ -171,7 +159,8 @@ public Authentication authenticate(Authentication authentication) throws Authent authorizationCodeAuthentication.getAuthorizationExchange(), oidcUser, mappedAuthorities, - accessToken); + accessTokenResponse.getAccessToken(), + accessTokenResponse.getRefreshToken()); authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); return authenticationResult; @@ -193,15 +182,24 @@ public boolean supports(Class authentication) { return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication); } + private OidcIdToken createOidcToken(ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse) { + JwtDecoder jwtDecoder = getJwtDecoder(clientRegistration); + Jwt jwt = jwtDecoder.decode((String) accessTokenResponse.getAdditionalParameters().get( + OidcParameterNames.ID_TOKEN)); + OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()); + OidcTokenValidator.validateIdToken(idToken, clientRegistration); + return idToken; + } + private JwtDecoder getJwtDecoder(ClientRegistration clientRegistration) { JwtDecoder jwtDecoder = this.jwtDecoders.get(clientRegistration.getRegistrationId()); if (jwtDecoder == null) { if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) { OAuth2Error oauth2Error = new OAuth2Error( - MISSING_SIGNATURE_VERIFIER_ERROR_CODE, - "Failed to find a Signature Verifier for Client Registration: '" + - clientRegistration.getRegistrationId() + "'. Check to ensure you have configured the JwkSet URI.", - null + MISSING_SIGNATURE_VERIFIER_ERROR_CODE, + "Failed to find a Signature Verifier for Client Registration: '" + + clientRegistration.getRegistrationId() + "'. Check to ensure you have configured the JwkSet URI.", + null ); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } @@ -210,88 +208,4 @@ private JwtDecoder getJwtDecoder(ClientRegistration clientRegistration) { } return jwtDecoder; } - - private void validateIdToken(OidcIdToken idToken, ClientRegistration clientRegistration) { - // 3.1.3.7 ID Token Validation - // http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation - - // Validate REQUIRED Claims - URL issuer = idToken.getIssuer(); - if (issuer == null) { - this.throwInvalidIdTokenException(); - } - String subject = idToken.getSubject(); - if (subject == null) { - this.throwInvalidIdTokenException(); - } - List audience = idToken.getAudience(); - if (CollectionUtils.isEmpty(audience)) { - this.throwInvalidIdTokenException(); - } - Instant expiresAt = idToken.getExpiresAt(); - if (expiresAt == null) { - this.throwInvalidIdTokenException(); - } - Instant issuedAt = idToken.getIssuedAt(); - if (issuedAt == null) { - this.throwInvalidIdTokenException(); - } - - // 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) - // MUST exactly match the value of the iss (issuer) Claim. - // TODO Depends on gh-4413 - - // 3. The Client MUST validate that the aud (audience) Claim contains its client_id value - // registered at the Issuer identified by the iss (issuer) Claim as an audience. - // The aud (audience) Claim MAY contain an array with more than one element. - // The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, - // or if it contains additional audiences not trusted by the Client. - if (!audience.contains(clientRegistration.getClientId())) { - this.throwInvalidIdTokenException(); - } - - // 4. If the ID Token contains multiple audiences, - // the Client SHOULD verify that an azp Claim is present. - String authorizedParty = idToken.getAuthorizedParty(); - if (audience.size() > 1 && authorizedParty == null) { - this.throwInvalidIdTokenException(); - } - - // 5. If an azp (authorized party) Claim is present, - // the Client SHOULD verify that its client_id is the Claim Value. - if (authorizedParty != null && !authorizedParty.equals(clientRegistration.getClientId())) { - this.throwInvalidIdTokenException(); - } - - // 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client - // in the id_token_signed_response_alg parameter during Registration. - // TODO Depends on gh-4413 - - // 9. The current time MUST be before the time represented by the exp Claim. - Instant now = Instant.now(); - if (!now.isBefore(expiresAt)) { - this.throwInvalidIdTokenException(); - } - - // 10. The iat Claim can be used to reject tokens that were issued too far away from the current time, - // limiting the amount of time that nonces need to be stored to prevent attacks. - // The acceptable range is Client specific. - Instant maxIssuedAt = Instant.now().plusSeconds(30); - if (issuedAt.isAfter(maxIssuedAt)) { - this.throwInvalidIdTokenException(); - } - - // 11. If a nonce value was sent in the Authentication Request, - // a nonce Claim MUST be present and its value checked to verify - // that it is the same value as the one that was sent in the Authentication Request. - // The Client SHOULD check the nonce value for replay attacks. - // The precise method for detecting replay attacks is Client specific. - // TODO Depends on gh-4442 - - } - - private void throwInvalidIdTokenException() { - OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE); - throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); - } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java new file mode 100644 index 00000000000..8f9e4bcbbae --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java @@ -0,0 +1,229 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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; + +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * An implementation of an {@link org.springframework.security.authentication.AuthenticationProvider} for OAuth 2.0 Login, + * which leverages the OAuth 2.0 Authorization Code Grant Flow. + * + * This {@link org.springframework.security.authentication.AuthenticationProvider} is responsible for authenticating + * an Authorization Code credential with the Authorization Server's Token Endpoint + * and if valid, exchanging it for an Access Token credential. + *

        + * It will also obtain the user attributes of the End-User (Resource Owner) + * from the UserInfo Endpoint using an {@link org.springframework.security.oauth2.client.userinfo.OAuth2UserService}, + * which will create a {@code Principal} in the form of an {@link OAuth2User}. + * The {@code OAuth2User} is then associated to the {@link OAuth2LoginAuthenticationToken} + * to complete the authentication. + * + * @author Rob Winch + * @since 5.1 + * @see OAuth2LoginAuthenticationToken + * @see ReactiveOAuth2AccessTokenResponseClient + * @see ReactiveOAuth2UserService + * @see OAuth2User + * @see Section 4.1 Authorization Code Grant Flow + * @see Section 4.1.3 Access Token Request + * @see Section 4.1.4 Access Token Response + */ +public class OidcAuthorizationCodeReactiveAuthenticationManager implements + ReactiveAuthenticationManager { + + private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter"; + private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter"; + private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token"; + private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier"; + + private final ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; + + private final ReactiveOAuth2UserService userService; + + private final ReactiveOAuth2AuthorizedClientService authorizedClientService; + + private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities); + + private Function decoderFactory = new DefaultDecoderFactory(); + + public OidcAuthorizationCodeReactiveAuthenticationManager( + ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient, + ReactiveOAuth2UserService userService, + ReactiveOAuth2AuthorizedClientService authorizedClientService) { + Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null"); + Assert.notNull(userService, "userService cannot be null"); + Assert.notNull(authorizedClientService, "authorizedClientService"); + this.accessTokenResponseClient = accessTokenResponseClient; + this.userService = userService; + this.authorizedClientService = authorizedClientService; + } + + @Override + public Mono authenticate(Authentication authentication) { + return Mono.defer(() -> { + OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication; + + // Section 3.1.2.1 Authentication Request - http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // scope REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. + if (!authorizationCodeAuthentication.getAuthorizationExchange() + .getAuthorizationRequest().getScopes().contains("openid")) { + // This is an OpenID Connect Authentication Request so return empty + // and let OAuth2LoginReactiveAuthenticationManager handle it instead + return Mono.empty(); + } + + + OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication + .getAuthorizationExchange().getAuthorizationRequest(); + OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication + .getAuthorizationExchange().getAuthorizationResponse(); + + if (authorizationResponse.statusError()) { + throw new OAuth2AuthenticationException( + authorizationResponse.getError(), authorizationResponse.getError().toString()); + } + + if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest( + authorizationCodeAuthentication.getClientRegistration(), + authorizationCodeAuthentication.getAuthorizationExchange()); + + return this.accessTokenResponseClient.getTokenResponse(authzRequest) + .flatMap(accessTokenResponse -> authenticationResult(authorizationCodeAuthentication, accessTokenResponse)); + }); + } + + /** + * Provides a way to customize the {@link ReactiveJwtDecoder} given a {@link ClientRegistration} + * @param decoderFactory the {@link Function} used to create {@link ReactiveJwtDecoder} instance. Cannot be null. + */ + void setDecoderFactory( + Function decoderFactory) { + Assert.notNull(decoderFactory, "decoderFactory cannot be null"); + this.decoderFactory = decoderFactory; + } + + private Mono authenticationResult(OAuth2LoginAuthenticationToken authorizationCodeAuthentication, OAuth2AccessTokenResponse accessTokenResponse) { + OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); + ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration(); + Map additionalParameters = accessTokenResponse.getAdditionalParameters(); + + if (!additionalParameters.containsKey(OidcParameterNames.ID_TOKEN)) { + OAuth2Error invalidIdTokenError = new OAuth2Error( + INVALID_ID_TOKEN_ERROR_CODE, + "Missing (required) ID Token in Token Response for Client Registration: " + clientRegistration.getRegistrationId(), + null); + throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); + } + + return createOidcToken(clientRegistration, accessTokenResponse) + .map(idToken -> new OidcUserRequest(clientRegistration, accessToken, idToken, additionalParameters)) + .flatMap(this.userService::loadUser) + .flatMap(oauth2User -> { + Collection mappedAuthorities = + this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities()); + + OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken( + authorizationCodeAuthentication.getClientRegistration(), + authorizationCodeAuthentication.getAuthorizationExchange(), + oauth2User, + mappedAuthorities, + accessToken); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + authenticationResult.getClientRegistration(), + authenticationResult.getName(), + authenticationResult.getAccessToken()); + OAuth2AuthenticationToken result = new OAuth2AuthenticationToken( + authenticationResult.getPrincipal(), + authenticationResult.getAuthorities(), + authenticationResult.getClientRegistration().getRegistrationId()); + return this.authorizedClientService.saveAuthorizedClient(authorizedClient, authenticationResult) + .thenReturn(result); + }); + } + + private Mono createOidcToken(ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse) { + ReactiveJwtDecoder jwtDecoder = this.decoderFactory.apply(clientRegistration); + String rawIdToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN); + return jwtDecoder.decode(rawIdToken) + .map(jwt -> new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims())) + .doOnNext(idToken -> OidcTokenValidator.validateIdToken(idToken, clientRegistration)); + } + + private static class DefaultDecoderFactory implements Function { + private final Map jwtDecoders = new ConcurrentHashMap<>(); + + @Override + public ReactiveJwtDecoder apply(ClientRegistration clientRegistration) { + ReactiveJwtDecoder jwtDecoder = this.jwtDecoders.get(clientRegistration.getRegistrationId()); + if (jwtDecoder == null) { + if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) { + OAuth2Error oauth2Error = new OAuth2Error( + MISSING_SIGNATURE_VERIFIER_ERROR_CODE, + "Failed to find a Signature Verifier for Client Registration: '" + + clientRegistration.getRegistrationId() + "'. Check to ensure you have configured the JwkSet URI.", + null + ); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + jwtDecoder = new NimbusReactiveJwtDecoder(clientRegistration.getProviderDetails().getJwkSetUri()); + this.jwtDecoders.put(clientRegistration.getRegistrationId(), jwtDecoder); + } + return jwtDecoder; + } + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidator.java new file mode 100644 index 00000000000..b646015a667 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidator.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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; + +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.oidc.OidcIdToken; +import org.springframework.util.CollectionUtils; + +import java.net.URL; +import java.time.Instant; +import java.util.List; + +/** + * @author Rob Winch + * @since 5.1 + */ +final class OidcTokenValidator { + private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token"; + + static void validateIdToken(OidcIdToken idToken, ClientRegistration clientRegistration) { + // 3.1.3.7 ID Token Validation + // http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + + // Validate REQUIRED Claims + URL issuer = idToken.getIssuer(); + if (issuer == null) { + throwInvalidIdTokenException(); + } + String subject = idToken.getSubject(); + if (subject == null) { + throwInvalidIdTokenException(); + } + List audience = idToken.getAudience(); + if (CollectionUtils.isEmpty(audience)) { + throwInvalidIdTokenException(); + } + Instant expiresAt = idToken.getExpiresAt(); + if (expiresAt == null) { + throwInvalidIdTokenException(); + } + Instant issuedAt = idToken.getIssuedAt(); + if (issuedAt == null) { + throwInvalidIdTokenException(); + } + + // 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) + // MUST exactly match the value of the iss (issuer) Claim. + // TODO Depends on gh-4413 + + // 3. The Client MUST validate that the aud (audience) Claim contains its client_id value + // registered at the Issuer identified by the iss (issuer) Claim as an audience. + // The aud (audience) Claim MAY contain an array with more than one element. + // The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, + // or if it contains additional audiences not trusted by the Client. + if (!audience.contains(clientRegistration.getClientId())) { + throwInvalidIdTokenException(); + } + + // 4. If the ID Token contains multiple audiences, + // the Client SHOULD verify that an azp Claim is present. + String authorizedParty = idToken.getAuthorizedParty(); + if (audience.size() > 1 && authorizedParty == null) { + throwInvalidIdTokenException(); + } + + // 5. If an azp (authorized party) Claim is present, + // the Client SHOULD verify that its client_id is the Claim Value. + if (authorizedParty != null && !authorizedParty.equals(clientRegistration.getClientId())) { + throwInvalidIdTokenException(); + } + + // 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client + // in the id_token_signed_response_alg parameter during Registration. + // TODO Depends on gh-4413 + + // 9. The current time MUST be before the time represented by the exp Claim. + Instant now = Instant.now(); + if (!now.isBefore(expiresAt)) { + throwInvalidIdTokenException(); + } + + // 10. The iat Claim can be used to reject tokens that were issued too far away from the current time, + // limiting the amount of time that nonces need to be stored to prevent attacks. + // The acceptable range is Client specific. + Instant maxIssuedAt = now.plusSeconds(30); + if (issuedAt.isAfter(maxIssuedAt)) { + throwInvalidIdTokenException(); + } + + // 11. If a nonce value was sent in the Authentication Request, + // a nonce Claim MUST be present and its value checked to verify + // that it is the same value as the one that was sent in the Authentication Request. + // The Client SHOULD check the nonce value for replay attacks. + // The precise method for detecting replay attacks is Client specific. + // TODO Depends on gh-4442 + + } + + private static void throwInvalidIdTokenException() { + OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE); + throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); + } + + private OidcTokenValidator() {} +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/NimbusUserInfoResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/NimbusUserInfoResponseClient.java deleted file mode 100644 index c63d905ec21..00000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/NimbusUserInfoResponseClient.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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.userinfo; - -import com.nimbusds.oauth2.sdk.ErrorObject; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.http.HTTPRequest; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; -import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse; -import com.nimbusds.openid.connect.sdk.UserInfoRequest; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.client.AbstractClientHttpResponse; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.http.converter.GenericHttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.security.authentication.AuthenticationServiceException; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.util.Assert; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.nio.charset.Charset; - -/** - * NOTE: This is a straight copy of org.springframework.security.oauth2.client.userinfo.NimbusUserInfoResponseClient - * - * @author Joe Grandja - * @since 5.0 - */ -final class NimbusUserInfoResponseClient { - private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response"; - private final GenericHttpMessageConverter genericHttpMessageConverter = new MappingJackson2HttpMessageConverter(); - - T getUserInfoResponse(OAuth2UserRequest userInfoRequest, Class returnType) throws OAuth2AuthenticationException { - ClientHttpResponse userInfoResponse = this.getUserInfoResponse( - userInfoRequest.getClientRegistration(), userInfoRequest.getAccessToken()); - try { - return (T) this.genericHttpMessageConverter.read(returnType, userInfoResponse); - } catch (IOException | HttpMessageNotReadableException ex) { - OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, - "An error occurred reading the UserInfo Success response: " + ex.getMessage(), null); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); - } - } - - T getUserInfoResponse(OAuth2UserRequest userInfoRequest, ParameterizedTypeReference typeReference) throws OAuth2AuthenticationException { - ClientHttpResponse userInfoResponse = this.getUserInfoResponse( - userInfoRequest.getClientRegistration(), userInfoRequest.getAccessToken()); - try { - return (T) this.genericHttpMessageConverter.read(typeReference.getType(), null, userInfoResponse); - } catch (IOException | HttpMessageNotReadableException ex) { - OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, - "An error occurred reading the UserInfo Success response: " + ex.getMessage(), null); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); - } - } - - private ClientHttpResponse getUserInfoResponse(ClientRegistration clientRegistration, - OAuth2AccessToken oauth2AccessToken) throws OAuth2AuthenticationException { - URI userInfoUri = URI.create(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()); - BearerAccessToken accessToken = new BearerAccessToken(oauth2AccessToken.getTokenValue()); - - UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken); - HTTPRequest httpRequest = userInfoRequest.toHTTPRequest(); - httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE); - httpRequest.setConnectTimeout(30000); - httpRequest.setReadTimeout(30000); - HTTPResponse httpResponse; - - try { - httpResponse = httpRequest.send(); - } catch (IOException ex) { - throw new AuthenticationServiceException("An error occurred while sending the UserInfo Request: " + - ex.getMessage(), ex); - } - - if (httpResponse.getStatusCode() == HTTPResponse.SC_OK) { - return new NimbusClientHttpResponse(httpResponse); - } - - UserInfoErrorResponse userInfoErrorResponse; - try { - userInfoErrorResponse = UserInfoErrorResponse.parse(httpResponse); - } catch (ParseException ex) { - OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, - "An error occurred parsing the UserInfo Error response: " + ex.getMessage(), null); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); - } - ErrorObject errorObject = userInfoErrorResponse.getErrorObject(); - - StringBuilder errorDescription = new StringBuilder(); - errorDescription.append("An error occurred while attempting to access the UserInfo Endpoint -> "); - errorDescription.append("Error details: ["); - errorDescription.append("UserInfo Uri: ").append(userInfoUri.toString()); - errorDescription.append(", Http Status: ").append(errorObject.getHTTPStatusCode()); - if (errorObject.getCode() != null) { - errorDescription.append(", Error Code: ").append(errorObject.getCode()); - } - if (errorObject.getDescription() != null) { - errorDescription.append(", Error Description: ").append(errorObject.getDescription()); - } - errorDescription.append("]"); - - OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, errorDescription.toString(), null); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); - } - - private static class NimbusClientHttpResponse extends AbstractClientHttpResponse { - private final HTTPResponse httpResponse; - private final HttpHeaders headers; - - private NimbusClientHttpResponse(HTTPResponse httpResponse) { - Assert.notNull(httpResponse, "httpResponse cannot be null"); - this.httpResponse = httpResponse; - this.headers = new HttpHeaders(); - this.headers.setAll(httpResponse.getHeaders()); - } - - @Override - public int getRawStatusCode() throws IOException { - return this.httpResponse.getStatusCode(); - } - - @Override - public String getStatusText() throws IOException { - return String.valueOf(this.getRawStatusCode()); - } - - @Override - public void close() { - } - - @Override - public InputStream getBody() throws IOException { - InputStream inputStream = new ByteArrayInputStream( - this.httpResponse.getContent().getBytes(Charset.forName("UTF-8"))); - return inputStream; - } - - @Override - public HttpHeaders getHeaders() { - return this.headers; - } - } -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserService.java new file mode 100644 index 00000000000..222a1e396d4 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserService.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.userinfo; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import reactor.core.publisher.Mono; + +/** + * An implementation of an {@link ReactiveOAuth2UserService} that supports OpenID Connect 1.0 Provider's. + * + * @author Rob Winch + * @since 5.1 + * @see ReactiveOAuth2UserService + * @see OidcUserRequest + * @see OidcUser + * @see DefaultOidcUser + * @see OidcUserInfo + */ +public class OidcReactiveOAuth2UserService implements + ReactiveOAuth2UserService { + + private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response"; + + private ReactiveOAuth2UserService oauth2UserService = new DefaultReactiveOAuth2UserService(); + + @Override + public Mono loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + Assert.notNull(userRequest, "userRequest cannot be null"); + return getUserInfo(userRequest) + .map(userInfo -> new OidcUserAuthority(userRequest.getIdToken(), userInfo)) + .defaultIfEmpty(new OidcUserAuthority(userRequest.getIdToken(), null)) + .map(authority -> { + OidcUserInfo userInfo = authority.getUserInfo(); + Set authorities = new HashSet<>(); + authorities.add(authority); + String userNameAttributeName = userRequest.getClientRegistration() + .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + if (StringUtils.hasText(userNameAttributeName)) { + return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo, userNameAttributeName); + } else { + return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo); + } + }); + } + + private Mono getUserInfo(OidcUserRequest userRequest) { + if (!OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest)) { + return Mono.empty(); + } + return this.oauth2UserService.loadUser(userRequest) + .map(OAuth2User::getAttributes) + .map(OidcUserInfo::new) + .doOnNext(userInfo -> { + String subject = userInfo.getSubject(); + if (subject == null || !subject.equals(userRequest.getIdToken().getSubject())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + }); + } + + public void setOauth2UserService(ReactiveOAuth2UserService oauth2UserService) { + Assert.notNull(oauth2UserService, "oauth2UserService cannot be null"); + this.oauth2UserService = oauth2UserService; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequest.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequest.java index 201ba577e2d..92158890b28 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequest.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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,6 +21,9 @@ import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.util.Assert; +import java.util.Collections; +import java.util.Map; + /** * Represents a request the {@link OidcUserService} uses * when initiating a request to the UserInfo Endpoint. @@ -45,7 +48,22 @@ public class OidcUserRequest extends OAuth2UserRequest { public OidcUserRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken, OidcIdToken idToken) { - super(clientRegistration, accessToken); + this(clientRegistration, accessToken, idToken, Collections.emptyMap()); + } + + /** + * Constructs an {@code OidcUserRequest} using the provided parameters. + * + * @since 5.1 + * @param clientRegistration the client registration + * @param accessToken the access token credential + * @param idToken the ID Token + * @param additionalParameters the additional parameters, may be empty + */ + public OidcUserRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken, + OidcIdToken idToken, Map additionalParameters) { + + super(clientRegistration, accessToken, additionalParameters); Assert.notNull(idToken, "idToken cannot be null"); this.idToken = idToken; } 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 new file mode 100644 index 00000000000..797528ceb4c --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.userinfo; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * Utilities for working with the {@link OidcUserRequest} + * + * @author Rob Winch + * @since 5.1 + */ +final class OidcUserRequestUtils { + + /** + * Determines if an {@link OidcUserRequest} should attempt to retrieve the user info endpoint. Will return true if + * all of the following are true: + * + *

          + *
        • The user info endpoint is defined on the ClientRegistration
        • + *
        • The Client Registration uses the {@link AuthorizationGrantType#AUTHORIZATION_CODE} and scopes in the + * access token are defined in the {@link ClientRegistration}
        • + *
        + * @param userRequest + * @return + */ + static boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) { + // Auto-disabled if UserInfo Endpoint URI is not provided + ClientRegistration clientRegistration = userRequest.getClientRegistration(); + if (StringUtils.isEmpty(clientRegistration.getProviderDetails() + .getUserInfoEndpoint().getUri())) { + + return false; + } + + // The Claims requested by the profile, email, address, and phone scope values + // are returned from the UserInfo Endpoint (as described in Section 5.3.2), + // when a response_type value is used that results in an Access Token being issued. + // However, when no Access Token is issued, which is the case for the response_type=id_token, + // the resulting Claims are returned in the ID Token. + // The Authorization Code Grant Flow, which is response_type=code, results in an Access Token being issued. + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { + + // Return true if there is at least one match between the authorized scope(s) and UserInfo scope(s) + return CollectionUtils + .containsAny(userRequest.getAccessToken().getScopes(), userRequest.getClientRegistration().getScopes()); + } + + return false; + } + + private OidcUserRequestUtils() {} +} 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 e354608c3db..f08c5ac2007 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -15,8 +15,9 @@ */ package org.springframework.security.oauth2.client.oidc.userinfo; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.security.core.GrantedAuthority; +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.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -26,12 +27,14 @@ import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; -import java.util.Map; import java.util.Set; /** @@ -49,20 +52,25 @@ public class OidcUserService implements OAuth2UserService userInfoScopes = new HashSet<>( Arrays.asList(OidcScopes.PROFILE, OidcScopes.EMAIL, OidcScopes.ADDRESS, OidcScopes.PHONE)); - private NimbusUserInfoResponseClient userInfoResponseClient = new NimbusUserInfoResponseClient(); + private OAuth2UserService oauth2UserService = new DefaultOAuth2UserService(); @Override public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { Assert.notNull(userRequest, "userRequest cannot be null"); OidcUserInfo userInfo = null; if (this.shouldRetrieveUserInfo(userRequest)) { - ParameterizedTypeReference> typeReference = - new ParameterizedTypeReference>() {}; - Map userAttributes = this.userInfoResponseClient.getUserInfoResponse(userRequest, typeReference); - userInfo = new OidcUserInfo(userAttributes); + OAuth2User oauth2User = this.oauth2UserService.loadUser(userRequest); + userInfo = new OidcUserInfo(oauth2User.getAttributes()); // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse - // Due to the possibility of token substitution attacks (see Section 16.11), + + // 1) The sub (subject) Claim MUST always be returned in the UserInfo Response + if (userInfo.getSubject() == null) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + // 2) Due to the possibility of token substitution attacks (see Section 16.11), // the UserInfo Response is not guaranteed to be about the End-User // identified by the sub (subject) element of the ID Token. // The sub Claim in the UserInfo Response MUST be verified to exactly match @@ -74,9 +82,8 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio } } - GrantedAuthority authority = new OidcUserAuthority(userRequest.getIdToken(), userInfo); - Set authorities = new HashSet<>(); - authorities.add(authority); + Set authorities = Collections.singleton( + new OidcUserAuthority(userRequest.getIdToken(), userInfo)); OidcUser user; @@ -109,9 +116,20 @@ private boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) { userRequest.getClientRegistration().getAuthorizationGrantType())) { // Return true if there is at least one match between the authorized scope(s) and UserInfo scope(s) - return userRequest.getAccessToken().getScopes().stream().anyMatch(userInfoScopes::contains); + return CollectionUtils.containsAny(userRequest.getAccessToken().getScopes(), this.userInfoScopes); } return false; } + + /** + * Sets the {@link OAuth2UserService} used when requesting the user info resource. + * + * @since 5.1 + * @param oauth2UserService the {@link OAuth2UserService} used when requesting the user info resource. + */ + public final void setOauth2UserService(OAuth2UserService oauth2UserService) { + Assert.notNull(oauth2UserService, "oauth2UserService cannot be null"); + this.oauth2UserService = oauth2UserService; + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index 54e730717ed..411cd36712a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -15,12 +15,14 @@ */ package org.springframework.security.oauth2.client.registration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; @@ -196,6 +198,7 @@ public String getJwkSetUri() { */ public class UserInfoEndpoint { private String uri; + private AuthenticationMethod authenticationMethod = AuthenticationMethod.HEADER; private String userNameAttributeName; private UserInfoEndpoint() { @@ -210,6 +213,16 @@ public String getUri() { return this.uri; } + /** + * Returns the authentication method for the user info endpoint. + * + * @since 5.1 + * @return the {@link AuthenticationMethod} for the user info endpoint. + */ + public AuthenticationMethod getAuthenticationMethod() { + return this.authenticationMethod; + } + /** * Returns the attribute name used to access the user's name from the user info response. * @@ -246,6 +259,7 @@ public static class Builder { private String authorizationUri; private String tokenUri; private String userInfoUri; + private AuthenticationMethod userInfoAuthenticationMethod = AuthenticationMethod.HEADER; private String userNameAttributeName; private String jwkSetUri; private String clientName; @@ -254,6 +268,17 @@ private Builder(String registrationId) { this.registrationId = registrationId; } + /** + * Sets the registration id. + * + * @param registrationId the registration id + * @return the {@link Builder} + */ + public Builder registrationId(String registrationId) { + this.registrationId = registrationId; + return this; + } + /** * Sets the client identifier. * @@ -324,6 +349,20 @@ public Builder scope(String... scope) { return this; } + /** + * Sets the scope(s) used for the client. + * + * @param scope the scope(s) used for the client + * @return the {@link Builder} + */ + public Builder scope(Collection scope) { + if (scope != null && !scope.isEmpty()) { + this.scopes = Collections.unmodifiableSet( + new LinkedHashSet<>(scope)); + } + return this; + } + /** * Sets the uri for the authorization endpoint. * @@ -357,6 +396,18 @@ public Builder userInfoUri(String userInfoUri) { return this; } + /** + * Sets the authentication method for the user info endpoint. + * + * @since 5.1 + * @param userInfoAuthenticationMethod the authentication method for the user info endpoint + * @return the {@link Builder} + */ + public Builder userInfoAuthenticationMethod(AuthenticationMethod userInfoAuthenticationMethod) { + this.userInfoAuthenticationMethod = userInfoAuthenticationMethod; + return this; + } + /** * Sets the attribute name used to access the user's name from the user info response. * @@ -397,7 +448,9 @@ public Builder clientName(String clientName) { */ public ClientRegistration build() { Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null"); - if (AuthorizationGrantType.IMPLICIT.equals(this.authorizationGrantType)) { + if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(this.authorizationGrantType)) { + this.validateClientCredentialsGrantType(); + } else if (AuthorizationGrantType.IMPLICIT.equals(this.authorizationGrantType)) { this.validateImplicitGrantType(); } else { this.validateAuthorizationCodeGrantType(); @@ -410,7 +463,7 @@ private ClientRegistration create() { clientRegistration.registrationId = this.registrationId; clientRegistration.clientId = this.clientId; - clientRegistration.clientSecret = this.clientSecret; + clientRegistration.clientSecret = StringUtils.hasText(this.clientSecret) ? this.clientSecret : ""; clientRegistration.clientAuthenticationMethod = this.clientAuthenticationMethod; clientRegistration.authorizationGrantType = this.authorizationGrantType; clientRegistration.redirectUriTemplate = this.redirectUriTemplate; @@ -420,11 +473,13 @@ private ClientRegistration create() { providerDetails.authorizationUri = this.authorizationUri; providerDetails.tokenUri = this.tokenUri; providerDetails.userInfoEndpoint.uri = this.userInfoUri; + providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod; providerDetails.userInfoEndpoint.userNameAttributeName = this.userNameAttributeName; providerDetails.jwkSetUri = this.jwkSetUri; clientRegistration.providerDetails = providerDetails; - clientRegistration.clientName = this.clientName; + clientRegistration.clientName = StringUtils.hasText(this.clientName) ? + this.clientName : this.registrationId; return clientRegistration; } @@ -434,17 +489,9 @@ private void validateAuthorizationCodeGrantType() { () -> "authorizationGrantType must be " + AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); Assert.hasText(this.registrationId, "registrationId cannot be empty"); Assert.hasText(this.clientId, "clientId cannot be empty"); - Assert.hasText(this.clientSecret, "clientSecret cannot be empty"); - Assert.notNull(this.clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); Assert.hasText(this.redirectUriTemplate, "redirectUriTemplate cannot be empty"); - Assert.notEmpty(this.scopes, "scopes cannot be empty"); Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty"); Assert.hasText(this.tokenUri, "tokenUri cannot be empty"); - if (this.scopes.contains(OidcScopes.OPENID)) { - // OIDC Clients need to verify/validate the ID Token - Assert.hasText(this.jwkSetUri, "jwkSetUri cannot be empty"); - } - Assert.hasText(this.clientName, "clientName cannot be empty"); } private void validateImplicitGrantType() { @@ -453,9 +500,15 @@ private void validateImplicitGrantType() { Assert.hasText(this.registrationId, "registrationId cannot be empty"); Assert.hasText(this.clientId, "clientId cannot be empty"); Assert.hasText(this.redirectUriTemplate, "redirectUriTemplate cannot be empty"); - Assert.notEmpty(this.scopes, "scopes cannot be empty"); Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty"); - Assert.hasText(this.clientName, "clientName cannot be empty"); + } + + private void validateClientCredentialsGrantType() { + Assert.isTrue(AuthorizationGrantType.CLIENT_CREDENTIALS.equals(this.authorizationGrantType), + () -> "authorizationGrantType must be " + AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()); + Assert.hasText(this.registrationId, "registrationId cannot be empty"); + Assert.hasText(this.clientId, "clientId cannot be empty"); + Assert.hasText(this.tokenUri, "tokenUri cannot be empty"); } } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java new file mode 100644 index 00000000000..3f97322e158 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.net.URI; +import java.util.Collections; +import java.util.List; + +import com.nimbusds.oauth2.sdk.GrantType; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; + +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.web.client.RestTemplate; + +/** + * Allows creating a {@link ClientRegistration.Builder} from an + * OpenID Provider Configuration. + * + * @author Rob Winch + * @author Josh Cummings + * @since 5.1 + */ +public class ClientRegistrations { + + /** + * Creates a {@link ClientRegistration.Builder} using the provided + * Issuer by making an + * OpenID Provider + * Configuration Request and using the values in the + * OpenID + * Provider Configuration Response to initialize the {@link ClientRegistration.Builder}. + * + *

        + * For example, if the issuer provided is "https://example.com", then an "OpenID Provider Configuration Request" will + * be made to "https://example.com/.well-known/openid-configuration". The result is expected to be an "OpenID + * Provider Configuration Response". + *

        + * + *

        + * Example usage: + *

        + *
        +	 * ClientRegistration registration = ClientRegistrations.fromOidcIssuerLocation("https://example.com")
        +	 *     .clientId("client-id")
        +	 *     .clientSecret("client-secret")
        +	 *     .build();
        +	 * 
        + * @param issuer the Issuer + * @return a {@link ClientRegistration.Builder} that was initialized by the OpenID Provider Configuration. + */ + public static ClientRegistration.Builder fromOidcIssuerLocation(String issuer) { + String openidConfiguration = getOpenidConfiguration(issuer); + OIDCProviderMetadata metadata = parse(openidConfiguration); + String metadataIssuer = metadata.getIssuer().getValue(); + if (!issuer.equals(metadataIssuer)) { + throw new IllegalStateException("The Issuer \"" + metadataIssuer + "\" provided in the OpenID Configuration did not match the requested issuer \"" + issuer + "\""); + } + + String name = URI.create(issuer).getHost(); + ClientAuthenticationMethod method = getClientAuthenticationMethod(issuer, metadata.getTokenEndpointAuthMethods()); + List grantTypes = metadata.getGrantTypes(); + // If null, the default includes authorization_code + if (grantTypes != null && !grantTypes.contains(GrantType.AUTHORIZATION_CODE)) { + throw new IllegalArgumentException("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + issuer + "\" returned a configuration of " + grantTypes); + } + List scopes = getScopes(metadata); + return ClientRegistration.withRegistrationId(name) + .userNameAttributeName(IdTokenClaimNames.SUB) + .scope(scopes) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .clientAuthenticationMethod(method) + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString()) + .jwkSetUri(metadata.getJWKSetURI().toASCIIString()) + .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString()) + .tokenUri(metadata.getTokenEndpointURI().toASCIIString()) + .clientName(issuer); + } + + private static String getOpenidConfiguration(String issuer) { + RestTemplate rest = new RestTemplate(); + try { + return rest.getForObject(issuer + "/.well-known/openid-configuration", String.class); + } catch(RuntimeException e) { + throw new IllegalArgumentException("Unable to resolve the OpenID Configuration with the provided Issuer of \"" + issuer + "\"", e); + } + } + + private static ClientAuthenticationMethod getClientAuthenticationMethod(String issuer, List metadataAuthMethods) { + if (metadataAuthMethods == null || metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) { + // If null, the default includes client_secret_basic + return ClientAuthenticationMethod.BASIC; + } + if (metadataAuthMethods.contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_POST)) { + return ClientAuthenticationMethod.POST; + } + throw new IllegalArgumentException("Only ClientAuthenticationMethod.BASIC and ClientAuthenticationMethod.POST are supported. The issuer \"" + issuer + "\" returned a configuration of " + metadataAuthMethods); + } + + private static List getScopes(OIDCProviderMetadata metadata) { + Scope scope = metadata.getScopes(); + if (scope == null) { + // If null, default to "openid" which must be supported + return Collections.singletonList(OidcScopes.OPENID); + } else { + return scope.toStringList(); + } + } + + private static OIDCProviderMetadata parse(String body) { + try { + return OIDCProviderMetadata.parse(body); + } + catch (ParseException e) { + throw new RuntimeException(e); + } + } + + private ClientRegistrations() {} + +} 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 90a79a7bf80..83d35f267f7 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 @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.client.userinfo; + import java.net.UnknownHostException; import java.util.HashSet; import java.util.Map; @@ -24,8 +25,10 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; @@ -97,10 +100,22 @@ public Mono loadUser(OAuth2UserRequest userRequest) ParameterizedTypeReference> typeReference = new ParameterizedTypeReference>() { }; - Mono> userAttributes = this.webClient.get() - .uri(userInfoUri) - .header(HttpHeaders.AUTHORIZATION, - "Bearer " + userRequest.getAccessToken().getTokenValue()) + AuthenticationMethod authenticationMethod = userRequest.getClientRegistration().getProviderDetails() + .getUserInfoEndpoint().getAuthenticationMethod(); + WebClient.RequestHeadersSpec requestHeadersSpec; + if (AuthenticationMethod.FORM.equals(authenticationMethod)) { + requestHeadersSpec = this.webClient.post() + .uri(userInfoUri) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .syncBody("access_token=" + userRequest.getAccessToken().getTokenValue()); + } else { + requestHeadersSpec = this.webClient.get() + .uri(userInfoUri) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .headers(headers -> headers.setBearerAuth(userRequest.getAccessToken().getTokenValue())); + } + Mono> userAttributes = requestHeadersSpec .retrieve() .onStatus(s -> s != HttpStatus.OK, response -> { return parse(response).map(userInfoErrorResponse -> { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java index 52929a04a73..cbbe7597882 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java @@ -32,6 +32,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -79,8 +80,11 @@ private ClientHttpResponse getUserInfoResponse(ClientRegistration clientRegistra OAuth2AccessToken oauth2AccessToken) throws OAuth2AuthenticationException { URI userInfoUri = URI.create(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()); BearerAccessToken accessToken = new BearerAccessToken(oauth2AccessToken.getTokenValue()); + AuthenticationMethod authenticationMethod = clientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod(); + HTTPRequest.Method httpMethod = AuthenticationMethod.FORM.equals(authenticationMethod) + ? HTTPRequest.Method.POST : HTTPRequest.Method.GET; - UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken); + UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, httpMethod, accessToken); HTTPRequest httpRequest = userInfoRequest.toHTTPRequest(); httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE); httpRequest.setConnectTimeout(30000); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequest.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequest.java index 949fb7699b9..b887c7aa440 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequest.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -18,6 +18,11 @@ import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; /** * Represents a request the {@link OAuth2UserService} uses @@ -32,6 +37,7 @@ public class OAuth2UserRequest { private final ClientRegistration clientRegistration; private final OAuth2AccessToken accessToken; + private final Map additionalParameters; /** * Constructs an {@code OAuth2UserRequest} using the provided parameters. @@ -40,10 +46,26 @@ public class OAuth2UserRequest { * @param accessToken the access token */ public OAuth2UserRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken) { + this(clientRegistration, accessToken, Collections.emptyMap()); + } + + /** + * Constructs an {@code OAuth2UserRequest} using the provided parameters. + * + * @since 5.1 + * @param clientRegistration the client registration + * @param accessToken the access token + * @param additionalParameters the additional parameters, may be empty + */ + public OAuth2UserRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken, + Map additionalParameters) { Assert.notNull(clientRegistration, "clientRegistration cannot be null"); Assert.notNull(accessToken, "accessToken cannot be null"); this.clientRegistration = clientRegistration; this.accessToken = accessToken; + this.additionalParameters = Collections.unmodifiableMap( + CollectionUtils.isEmpty(additionalParameters) ? + Collections.emptyMap() : new LinkedHashMap<>(additionalParameters)); } /** @@ -63,4 +85,14 @@ public ClientRegistration getClientRegistration() { public OAuth2AccessToken getAccessToken() { return this.accessToken; } + + /** + * Returns the additional parameters that may be used in the request. + * + * @since 5.1 + * @return a {@code Map} of the additional parameters, may be empty. + */ + public Map getAdditionalParameters() { + return this.additionalParameters; + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java new file mode 100644 index 00000000000..5e4f9a59e75 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * An implementation of an {@link OAuth2AuthorizedClientRepository} that + * delegates to the provided {@link OAuth2AuthorizedClientService} if the current + * {@code Principal} is authenticated, otherwise, + * to the default (or provided) {@link OAuth2AuthorizedClientRepository} + * if the current request is unauthenticated (or anonymous). + * The default {@code OAuth2AuthorizedClientRepository} is {@link HttpSessionOAuth2AuthorizedClientRepository}. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizedClientRepository + * @see OAuth2AuthorizedClient + * @see OAuth2AuthorizedClientService + * @see HttpSessionOAuth2AuthorizedClientRepository + */ +public final class AuthenticatedPrincipalOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository { + private final AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl(); + private final OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository anonymousAuthorizedClientRepository = new HttpSessionOAuth2AuthorizedClientRepository(); + + /** + * Constructs a {@code AuthenticatedPrincipalOAuth2AuthorizedClientRepository} using the provided parameters. + * + * @param authorizedClientService the authorized client service + */ + public AuthenticatedPrincipalOAuth2AuthorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) { + Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); + this.authorizedClientService = authorizedClientService; + } + + /** + * Sets the {@link OAuth2AuthorizedClientRepository} used for requests that are unauthenticated (or anonymous). + * The default is {@link HttpSessionOAuth2AuthorizedClientRepository}. + * + * @param anonymousAuthorizedClientRepository the repository used for requests that are unauthenticated (or anonymous) + */ + public final void setAnonymousAuthorizedClientRepository(OAuth2AuthorizedClientRepository anonymousAuthorizedClientRepository) { + Assert.notNull(anonymousAuthorizedClientRepository, "anonymousAuthorizedClientRepository cannot be null"); + this.anonymousAuthorizedClientRepository = anonymousAuthorizedClientRepository; + } + + @Override + public T loadAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request) { + if (this.isPrincipalAuthenticated(principal)) { + return this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, principal.getName()); + } else { + return this.anonymousAuthorizedClientRepository.loadAuthorizedClient(clientRegistrationId, principal, request); + } + } + + @Override + public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, + HttpServletRequest request, HttpServletResponse response) { + if (this.isPrincipalAuthenticated(principal)) { + this.authorizedClientService.saveAuthorizedClient(authorizedClient, principal); + } else { + this.anonymousAuthorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, request, response); + } + } + + @Override + public void removeAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request, HttpServletResponse response) { + if (this.isPrincipalAuthenticated(principal)) { + this.authorizedClientService.removeAuthorizedClient(clientRegistrationId, principal.getName()); + } else { + this.anonymousAuthorizedClientRepository.removeAuthorizedClient(clientRegistrationId, principal, request, response); + } + } + + private boolean isPrincipalAuthenticated(Authentication authentication) { + return authentication != null && + !this.authenticationTrustResolver.isAnonymous(authentication) && + authentication.isAuthenticated(); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java index 9e316f0fa7a..1c7ed342dd5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java @@ -63,9 +63,23 @@ void saveAuthorizationRequest(T authorizationRequest, HttpServletRequest request * Removes and returns the {@link OAuth2AuthorizationRequest} associated to the * provided {@code HttpServletRequest} or if not available returns {@code null}. * + * @deprecated Use {@link #removeAuthorizationRequest(HttpServletRequest, HttpServletResponse)} instead * @param request the {@code HttpServletRequest} * @return the removed {@link OAuth2AuthorizationRequest} or {@code null} if not available */ + @Deprecated T removeAuthorizationRequest(HttpServletRequest request); + /** + * Removes and returns the {@link OAuth2AuthorizationRequest} associated to the + * provided {@code HttpServletRequest} and {@code HttpServletResponse} or if not available returns {@code null}. + * + * @since 5.1 + * @param request the {@code HttpServletRequest} + * @param response the {@code HttpServletResponse} + * @return the {@link OAuth2AuthorizationRequest} or {@code null} if not available + */ + default T removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) { + return removeAuthorizationRequest(request); + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java new file mode 100644 index 00000000000..18a443b6c43 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.web.util.UrlUtils; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.Assert; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.servlet.http.HttpServletRequest; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import static org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME; + +/** + * An implementation of an {@link OAuth2AuthorizationRequestResolver} that attempts to + * resolve an {@link OAuth2AuthorizationRequest} from the provided {@code HttpServletRequest} + * using the default request {@code URI} pattern {@code /oauth2/authorization/{registrationId}}. + * + *

        + * NOTE: The default base {@code URI} {@code /oauth2/authorization} may be overridden + * via it's constructor {@link #DefaultOAuth2AuthorizationRequestResolver(ClientRegistrationRepository, String)}. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizationRequestResolver + * @see OAuth2AuthorizationRequestRedirectFilter + */ +public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"; + private final ClientRegistrationRepository clientRegistrationRepository; + private final AntPathRequestMatcher authorizationRequestMatcher; + private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); + + /** + * Constructs a {@code DefaultOAuth2AuthorizationRequestResolver} using the provided parameters. + * + * @param clientRegistrationRepository the repository of client registrations + * @param authorizationRequestBaseUri the base {@code URI} used for resolving authorization requests + */ + public DefaultOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository, + String authorizationRequestBaseUri) { + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty"); + this.clientRegistrationRepository = clientRegistrationRepository; + this.authorizationRequestMatcher = new AntPathRequestMatcher( + authorizationRequestBaseUri + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}"); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + String registrationId = this.resolveRegistrationId(request); + if (registrationId == null) { + return null; + } + + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); + if (clientRegistration == null) { + throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); + } + + OAuth2AuthorizationRequest.Builder builder; + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { + builder = OAuth2AuthorizationRequest.authorizationCode(); + } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { + builder = OAuth2AuthorizationRequest.implicit(); + } else { + throw new IllegalArgumentException("Invalid Authorization Grant Type (" + + clientRegistration.getAuthorizationGrantType().getValue() + + ") for Client Registration with Id: " + clientRegistration.getRegistrationId()); + } + + String redirectUriAction = this.resolveRedirectUriAction(request, clientRegistration); + String redirectUriStr = this.expandRedirectUri(request, clientRegistration, redirectUriAction); + + Map additionalParameters = new HashMap<>(); + additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); + + OAuth2AuthorizationRequest authorizationRequest = builder + .clientId(clientRegistration.getClientId()) + .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .redirectUri(redirectUriStr) + .scopes(clientRegistration.getScopes()) + .state(this.stateGenerator.generateKey()) + .additionalParameters(additionalParameters) + .build(); + + return authorizationRequest; + } + + private String resolveRegistrationId(HttpServletRequest request) { + // Check for ClientAuthorizationRequiredException which may have been set + // in the request by OAuth2AuthorizationRequestRedirectFilter + ClientAuthorizationRequiredException authzEx = + (ClientAuthorizationRequiredException) request.getAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME); + if (authzEx != null) { + return authzEx.getClientRegistrationId(); + } + if (this.authorizationRequestMatcher.matches(request)) { + return this.authorizationRequestMatcher + .extractUriTemplateVariables(request).get(REGISTRATION_ID_URI_VARIABLE_NAME); + } + return null; + } + + private String resolveRedirectUriAction(HttpServletRequest request, ClientRegistration clientRegistration) { + String action = null; + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { + String loginAction = "login"; + String authorizeAction = "authorize"; + String actionParameter = request.getParameter("action"); + if (request.getAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME) != null) { + // Check for ClientAuthorizationRequiredException which may have been set + // in the request by OAuth2AuthorizationRequestRedirectFilter + action = authorizeAction; + } else if (actionParameter == null) { + action = loginAction; // Default + } else { + if (actionParameter.equalsIgnoreCase(loginAction)) { + action = loginAction; + } else { + action = authorizeAction; + } + } + } + return action; + } + + private String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration, String action) { + // Supported URI variables -> baseUrl, action, registrationId + // Used in -> CommonOAuth2Provider.DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}" + Map uriVariables = new HashMap<>(); + uriVariables.put("registrationId", clientRegistration.getRegistrationId()); + String baseUrl = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) + .replaceQuery(null) + .replacePath(request.getContextPath()) + .build() + .toUriString(); + uriVariables.put("baseUrl", baseUrl); + if (action != null) { + uriVariables.put("action", action); + } + return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUriTemplate()) + .buildAndExpand(uriVariables) + .toUriString(); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java index 09e1b6bd713..3cc9517e6f3 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java @@ -58,7 +58,7 @@ public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationReq Assert.notNull(request, "request cannot be null"); Assert.notNull(response, "response cannot be null"); if (authorizationRequest == null) { - this.removeAuthorizationRequest(request); + this.removeAuthorizationRequest(request, response); return; } String state = authorizationRequest.getState(); @@ -85,6 +85,12 @@ public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest return originalRequest; } + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) { + Assert.notNull(response, "response cannot be null"); + return this.removeAuthorizationRequest(request); + } + /** * Gets the state parameter from the {@link HttpServletRequest} * @param request the request to use diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java new file mode 100644 index 00000000000..ce76392f352 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.HashMap; +import java.util.Map; + +/** + * An implementation of an {@link OAuth2AuthorizedClientRepository} that stores + * {@link OAuth2AuthorizedClient}'s in the {@code HttpSession}. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizedClientRepository + * @see OAuth2AuthorizedClient + */ +public final class HttpSessionOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository { + private static final String DEFAULT_AUTHORIZED_CLIENTS_ATTR_NAME = + HttpSessionOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS"; + private final String sessionAttributeName = DEFAULT_AUTHORIZED_CLIENTS_ATTR_NAME; + + @SuppressWarnings("unchecked") + @Override + public T loadAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request) { + Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); + Assert.notNull(request, "request cannot be null"); + return (T) this.getAuthorizedClients(request).get(clientRegistrationId); + } + + @Override + public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, + HttpServletRequest request, HttpServletResponse response) { + Assert.notNull(authorizedClient, "authorizedClient cannot be null"); + Assert.notNull(request, "request cannot be null"); + Assert.notNull(response, "response cannot be null"); + Map authorizedClients = this.getAuthorizedClients(request); + authorizedClients.put(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient); + request.getSession().setAttribute(this.sessionAttributeName, authorizedClients); + } + + @Override + public void removeAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request, HttpServletResponse response) { + Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); + Assert.notNull(request, "request cannot be null"); + Map authorizedClients = this.getAuthorizedClients(request); + if (!authorizedClients.isEmpty()) { + if (authorizedClients.remove(clientRegistrationId) != null) { + if (!authorizedClients.isEmpty()) { + request.getSession().setAttribute(this.sessionAttributeName, authorizedClients); + } else { + request.getSession().removeAttribute(this.sessionAttributeName); + } + } + } + } + + @SuppressWarnings("unchecked") + private Map getAuthorizedClients(HttpServletRequest request) { + HttpSession session = request.getSession(false); + Map authorizedClients = session == null ? null : + (Map) session.getAttribute(this.sessionAttributeName); + if (authorizedClients == null) { + authorizedClients = new HashMap<>(); + } + return authorizedClients; + } +} 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 62ce8868a1a..798f49831d0 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 @@ -15,19 +15,11 @@ */ package org.springframework.security.oauth2.client.web; -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider; import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -51,6 +43,12 @@ import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.UriComponentsBuilder; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + /** * A {@code Filter} for the OAuth 2.0 Authorization Code Grant, * which handles the processing of the OAuth 2.0 Authorization Response. @@ -74,7 +72,7 @@ * Upon a successful authentication, an {@link OAuth2AuthorizedClient Authorized Client} is created by associating the * {@link OAuth2AuthorizationCodeAuthenticationToken#getClientRegistration() client} to the * {@link OAuth2AuthorizationCodeAuthenticationToken#getAccessToken() access token} and current {@code Principal} - * and saving it via the {@link OAuth2AuthorizedClientService}. + * and saving it via the {@link OAuth2AuthorizedClientRepository}. * *

      * @@ -88,13 +86,13 @@ * @see OAuth2AuthorizationRequestRedirectFilter * @see ClientRegistrationRepository * @see OAuth2AuthorizedClient - * @see OAuth2AuthorizedClientService + * @see OAuth2AuthorizedClientRepository * @see Section 4.1 Authorization Code Grant * @see Section 4.1.2 Authorization Response */ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter { private final ClientRegistrationRepository clientRegistrationRepository; - private final OAuth2AuthorizedClientService authorizedClientService; + private final OAuth2AuthorizedClientRepository authorizedClientRepository; private final AuthenticationManager authenticationManager; private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); @@ -106,17 +104,17 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter { * Constructs an {@code OAuth2AuthorizationCodeGrantFilter} using the provided parameters. * * @param clientRegistrationRepository the repository of client registrations - * @param authorizedClientService the authorized client service + * @param authorizedClientRepository the authorized client repository * @param authenticationManager the authentication manager */ public OAuth2AuthorizationCodeGrantFilter(ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientService authorizedClientService, + OAuth2AuthorizedClientRepository authorizedClientRepository, AuthenticationManager authenticationManager) { Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); - Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); + Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); Assert.notNull(authenticationManager, "authenticationManager cannot be null"); this.clientRegistrationRepository = clientRegistrationRepository; - this.authorizedClientService = authorizedClientService; + this.authorizedClientRepository = authorizedClientRepository; this.authenticationManager = authenticationManager; } @@ -160,7 +158,8 @@ private boolean shouldProcessAuthorizationResponse(HttpServletRequest request) { private void processAuthorizationResponse(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request); + OAuth2AuthorizationRequest authorizationRequest = + this.authorizationRequestRepository.removeAuthorizationRequest(request, response); String registrationId = (String) authorizationRequest.getAdditionalParameters().get(OAuth2ParameterNames.REGISTRATION_ID); ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); @@ -194,13 +193,15 @@ private void processAuthorizationResponse(HttpServletRequest request, HttpServle } Authentication currentAuthentication = SecurityContextHolder.getContext().getAuthentication(); + String principalName = currentAuthentication != null ? currentAuthentication.getName() : "anonymousUser"; OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( authenticationResult.getClientRegistration(), - currentAuthentication.getName(), - authenticationResult.getAccessToken()); + principalName, + authenticationResult.getAccessToken(), + authenticationResult.getRefreshToken()); - this.authorizedClientService.saveAuthorizedClient(authorizedClient, currentAuthentication); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request, response); String redirectUrl = authorizationResponse.getRedirectUri(); SavedRequest savedRequest = this.requestCache.getRequest(request, response); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java index 44fe7270da7..ef6c12975d8 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java @@ -16,34 +16,24 @@ package org.springframework.security.oauth2.client.web; import org.springframework.http.HttpStatus; -import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; -import org.springframework.security.crypto.keygen.StringKeyGenerator; import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.util.ThrowableAnalyzer; -import org.springframework.security.web.util.UrlUtils; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.web.util.UriComponentsBuilder; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.net.URI; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; /** * This {@code Filter} initiates the authorization code grant or implicit grant flow @@ -58,19 +48,24 @@ * *

      * By default, this {@code Filter} responds to authorization requests - * at the {@code URI} {@code /oauth2/authorization/{registrationId}}. + * at the {@code URI} {@code /oauth2/authorization/{registrationId}} + * using the default {@link OAuth2AuthorizationRequestResolver}. * The {@code URI} template variable {@code {registrationId}} represents the * {@link ClientRegistration#getRegistrationId() registration identifier} of the client * that is used for initiating the OAuth 2.0 Authorization Request. * *

      - * NOTE: The default base {@code URI} {@code /oauth2/authorization} may be overridden - * via it's constructor {@link #OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository, String)}. + * The default base {@code URI} {@code /oauth2/authorization} may be overridden + * via the constructor {@link #OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository, String)}, + * or alternatively, an {@code OAuth2AuthorizationRequestResolver} may be provided to the constructor + * {@link #OAuth2AuthorizationRequestRedirectFilter(OAuth2AuthorizationRequestResolver)} + * to override the resolving of authorization requests. * @author Joe Grandja * @author Rob Winch * @since 5.0 * @see OAuth2AuthorizationRequest + * @see OAuth2AuthorizationRequestResolver * @see AuthorizationRequestRepository * @see ClientRegistration * @see ClientRegistrationRepository @@ -84,18 +79,14 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt * The default base {@code URI} used for authorization requests. */ public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization"; - private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"; - private static final String AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME = + static final String AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME = ClientAuthorizationRequiredException.class.getName() + ".AUTHORIZATION_REQUIRED_EXCEPTION"; - private final AntPathRequestMatcher authorizationRequestMatcher; - private final ClientRegistrationRepository clientRegistrationRepository; - private final OAuth2AuthorizationRequestUriBuilder authorizationRequestUriBuilder = new OAuth2AuthorizationRequestUriBuilder(); + private final ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); private final RedirectStrategy authorizationRedirectStrategy = new DefaultRedirectStrategy(); - private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); + private OAuth2AuthorizationRequestResolver authorizationRequestResolver; private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); private RequestCache requestCache = new HttpSessionRequestCache(); - private final ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); /** * Constructs an {@code OAuth2AuthorizationRequestRedirectFilter} using the provided parameters. @@ -112,14 +103,23 @@ public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository cli * @param clientRegistrationRepository the repository of client registrations * @param authorizationRequestBaseUri the base {@code URI} used for authorization requests */ - public OAuth2AuthorizationRequestRedirectFilter( - ClientRegistrationRepository clientRegistrationRepository, String authorizationRequestBaseUri) { - - Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty"); + public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository, + String authorizationRequestBaseUri) { Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); - this.authorizationRequestMatcher = new AntPathRequestMatcher( - authorizationRequestBaseUri + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}"); - this.clientRegistrationRepository = clientRegistrationRepository; + Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty"); + this.authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( + clientRegistrationRepository, authorizationRequestBaseUri); + } + + /** + * Constructs an {@code OAuth2AuthorizationRequestRedirectFilter} using the provided parameters. + * + * @since 5.1 + * @param authorizationRequestResolver the resolver used for resolving authorization requests + */ + public OAuth2AuthorizationRequestRedirectFilter(OAuth2AuthorizationRequestResolver authorizationRequestResolver) { + Assert.notNull(authorizationRequestResolver, "authorizationRequestResolver cannot be null"); + this.authorizationRequestResolver = authorizationRequestResolver; } /** @@ -147,12 +147,14 @@ public final void setRequestCache(RequestCache requestCache) { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - if (this.shouldRequestAuthorization(request, response)) { - try { - this.sendRedirectForAuthorization(request, response); - } catch (Exception failed) { - this.unsuccessfulRedirectForAuthorization(request, response, failed); + try { + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request); + if (authorizationRequest != null) { + this.sendRedirectForAuthorization(request, response, authorizationRequest); + return; } + } catch (Exception failed) { + this.unsuccessfulRedirectForAuthorization(request, response, failed); return; } @@ -168,7 +170,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (authzEx != null) { try { request.setAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME, authzEx); - this.sendRedirectForAuthorization(request, response, authzEx.getClientRegistrationId()); + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request); + if (authorizationRequest == null) { + throw authzEx; + } + this.sendRedirectForAuthorization(request, response, authorizationRequest); this.requestCache.saveRequest(request, response); } catch (Exception failed) { this.unsuccessfulRedirectForAuthorization(request, response, failed); @@ -188,61 +194,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } - private boolean shouldRequestAuthorization(HttpServletRequest request, HttpServletResponse response) { - return this.authorizationRequestMatcher.matches(request); - } - - private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException { - - String registrationId = this.authorizationRequestMatcher - .extractUriTemplateVariables(request).get(REGISTRATION_ID_URI_VARIABLE_NAME); - this.sendRedirectForAuthorization(request, response, registrationId); - } - private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response, - String registrationId) throws IOException, ServletException { - - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); - if (clientRegistration == null) { - throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); - } - this.sendRedirectForAuthorization(request, response, clientRegistration); - } - - private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response, - ClientRegistration clientRegistration) throws IOException, ServletException { - - String redirectUriStr = this.expandRedirectUri(request, clientRegistration); - - Map additionalParameters = new HashMap<>(); - additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); - - OAuth2AuthorizationRequest.Builder builder; - if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { - builder = OAuth2AuthorizationRequest.authorizationCode(); - } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { - builder = OAuth2AuthorizationRequest.implicit(); - } else { - throw new IllegalArgumentException("Invalid Authorization Grant Type (" + - clientRegistration.getAuthorizationGrantType().getValue() + - ") for Client Registration with Id: " + clientRegistration.getRegistrationId()); - } - OAuth2AuthorizationRequest authorizationRequest = builder - .clientId(clientRegistration.getClientId()) - .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) - .redirectUri(redirectUriStr) - .scopes(clientRegistration.getScopes()) - .state(this.stateGenerator.generateKey()) - .additionalParameters(additionalParameters) - .build(); + OAuth2AuthorizationRequest authorizationRequest) throws IOException, ServletException { if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) { this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); } - - URI redirectUri = this.authorizationRequestUriBuilder.build(authorizationRequest); - this.authorizationRedirectStrategy.sendRedirect(request, response, redirectUri.toString()); + this.authorizationRedirectStrategy.sendRedirect(request, response, authorizationRequest.getAuthorizationRequestUri()); } private void unsuccessfulRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response, @@ -254,43 +212,6 @@ private void unsuccessfulRedirectForAuthorization(HttpServletRequest request, Ht response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()); } - private String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration) { - // Supported URI variables -> baseUrl, action, registrationId - // Used in -> CommonOAuth2Provider.DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}" - Map uriVariables = new HashMap<>(); - uriVariables.put("registrationId", clientRegistration.getRegistrationId()); - - String baseUrl = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) - .replacePath(request.getContextPath()) - .build() - .toUriString(); - uriVariables.put("baseUrl", baseUrl); - - if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { - String loginAction = "login"; - String authorizeAction = "authorize"; - String actionParameter = "action"; - String action; - if (request.getAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME) != null) { - action = authorizeAction; - } else if (request.getParameter(actionParameter) == null) { - action = loginAction; - } else { - String actionValue = request.getParameter(actionParameter); - if (loginAction.equalsIgnoreCase(actionValue)) { - action = loginAction; - } else { - action = authorizeAction; - } - } - uriVariables.put("action", action); - } - - return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUriTemplate()) - .buildAndExpand(uriVariables) - .toUriString(); - } - private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer { protected void initExtractorMap() { super.initExtractorMap(); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java index f49eb4d6df0..ae89c614989 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilter.java @@ -87,7 +87,6 @@ public class OAuth2AuthorizationRequestRedirectWebFilter implements WebFilter { ClientAuthorizationRequiredException.class.getName() + ".AUTHORIZATION_REQUIRED_EXCEPTION"; private final ServerWebExchangeMatcher authorizationRequestMatcher; private final ReactiveClientRegistrationRepository clientRegistrationRepository; - private final OAuth2AuthorizationRequestUriBuilder authorizationRequestUriBuilder = new OAuth2AuthorizationRequestUriBuilder(); private final ServerRedirectStrategy authorizationRedirectStrategy = new DefaultServerRedirectStrategy(); private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); private ReactiveAuthorizationRequestRepository authorizationRequestRepository = @@ -136,6 +135,7 @@ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { .map(ServerWebExchangeMatcher.MatchResult::getVariables) .map(variables -> variables.get(REGISTRATION_ID_URI_VARIABLE_NAME)) .cast(String.class) + .onErrorResume(ClientAuthorizationRequiredException.class, e -> Mono.just(e.getClientRegistrationId())) .flatMap(clientRegistrationId -> this.findByRegistrationId(exchange, clientRegistrationId)) .flatMap(clientRegistration -> sendRedirectForAuthorization(exchange, clientRegistration)); } @@ -183,7 +183,9 @@ else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizat .saveAuthorizationRequest(authorizationRequest, exchange); } - URI redirectUri = this.authorizationRequestUriBuilder.build(authorizationRequest); + URI redirectUri = UriComponentsBuilder + .fromUriString(authorizationRequest.getAuthorizationRequestUri()) + .build(true).toUri(); return saveAuthorizationRequest .then(this.authorizationRedirectStrategy.sendRedirect(exchange, redirectUri)); }); @@ -197,6 +199,7 @@ private String expandRedirectUri(ServerHttpRequest request, ClientRegistration c String baseUrl = UriComponentsBuilder.fromHttpRequest(new ServerHttpRequestDecorator(request)) .replacePath(request.getPath().contextPath().value()) + .replaceQuery(null) .build() .toUriString(); uriVariables.put("baseUrl", baseUrl); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestResolver.java new file mode 100644 index 00000000000..1e6a9c03e60 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestResolver.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; + +import javax.servlet.http.HttpServletRequest; + +/** + * Implementations of this interface are capable of resolving + * an {@link OAuth2AuthorizationRequest} from the provided {@code HttpServletRequest}. + * Used by the {@link OAuth2AuthorizationRequestRedirectFilter} for resolving Authorization Requests. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizationRequest + * @see OAuth2AuthorizationRequestRedirectFilter + */ +public interface OAuth2AuthorizationRequestResolver { + + /** + * Returns the {@link OAuth2AuthorizationRequest} resolved from + * the provided {@code HttpServletRequest} or {@code null} if not available. + * + * @param request the {@code HttpServletRequest} + * @return the resolved {@link OAuth2AuthorizationRequest} or {@code null} if not available + */ + OAuth2AuthorizationRequest resolve(HttpServletRequest request); + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilder.java deleted file mode 100644 index 3a9635f9e9c..00000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilder.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://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.web; - -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; -import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.web.util.UriComponentsBuilder; - -import java.net.URI; -import java.util.Set; - -/** - * A {@code URI} builder for an OAuth 2.0 Authorization Request. - * - * @author Joe Grandja - * @since 5.0 - * @see OAuth2AuthorizationRequest - * @see Section 4.1.1 Authorization Code Grant Request - * @see Section 4.2.1 Implicit Grant Request - */ -class OAuth2AuthorizationRequestUriBuilder { - - URI build(OAuth2AuthorizationRequest authorizationRequest) { - Assert.notNull(authorizationRequest, "authorizationRequest cannot be null"); - Set scopes = authorizationRequest.getScopes(); - UriComponentsBuilder uriBuilder = UriComponentsBuilder - .fromUriString(authorizationRequest.getAuthorizationUri()) - .queryParam(OAuth2ParameterNames.RESPONSE_TYPE, authorizationRequest.getResponseType().getValue()) - .queryParam(OAuth2ParameterNames.CLIENT_ID, authorizationRequest.getClientId()) - .queryParam(OAuth2ParameterNames.SCOPE, StringUtils.collectionToDelimitedString(scopes, " ")) - .queryParam(OAuth2ParameterNames.STATE, authorizationRequest.getState()); - if (authorizationRequest.getRedirectUri() != null) { - uriBuilder.queryParam(OAuth2ParameterNames.REDIRECT_URI, authorizationRequest.getRedirectUri()); - } - - return uriBuilder.build().encode().toUri(); - } -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java new file mode 100644 index 00000000000..b18c785861f --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Implementations of this interface are responsible for the persistence + * of {@link OAuth2AuthorizedClient Authorized Client(s)} between requests. + * + *

      + * The primary purpose of an {@link OAuth2AuthorizedClient Authorized Client} + * is to associate an {@link OAuth2AuthorizedClient#getAccessToken() Access Token} credential + * to a {@link OAuth2AuthorizedClient#getClientRegistration() Client} and Resource Owner, + * who is the {@link OAuth2AuthorizedClient#getPrincipalName() Principal} + * that originally granted the authorization. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizedClient + * @see ClientRegistration + * @see Authentication + * @see OAuth2AccessToken + */ +public interface OAuth2AuthorizedClientRepository { + + /** + * Returns the {@link OAuth2AuthorizedClient} associated to the + * provided client registration identifier and End-User {@link Authentication} (Resource Owner) + * or {@code null} if not available. + * + * @param clientRegistrationId the identifier for the client's registration + * @param principal the End-User {@link Authentication} (Resource Owner) + * @param request the {@code HttpServletRequest} + * @param a type of OAuth2AuthorizedClient + * @return the {@link OAuth2AuthorizedClient} or {@code null} if not available + */ + T loadAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request); + + /** + * Saves the {@link OAuth2AuthorizedClient} associating it to + * the provided End-User {@link Authentication} (Resource Owner). + * + * @param authorizedClient the authorized client + * @param principal the End-User {@link Authentication} (Resource Owner) + * @param request the {@code HttpServletRequest} + * @param response the {@code HttpServletResponse} + */ + void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, + HttpServletRequest request, HttpServletResponse response); + + /** + * Removes the {@link OAuth2AuthorizedClient} associated to the + * provided client registration identifier and End-User {@link Authentication} (Resource Owner). + * + * @param clientRegistrationId the identifier for the client's registration + * @param principal the End-User {@link Authentication} (Resource Owner) + * @param request the {@code HttpServletRequest} + * @param response the {@code HttpServletResponse} + */ + void removeAuthorizedClient(String clientRegistrationId, Authentication principal, + HttpServletRequest request, HttpServletResponse response); + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java index 231b7f5c789..c44512e508e 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -15,12 +15,6 @@ */ package org.springframework.security.oauth2.client.web; -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -43,6 +37,11 @@ import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + /** * An implementation of an {@link AbstractAuthenticationProcessingFilter} for OAuth 2.0 Login. * @@ -68,7 +67,7 @@ * *

    • * Upon a successful authentication, an {@link OAuth2AuthenticationToken} is created (representing the End-User {@code Principal}) - * and associated to the {@link OAuth2AuthorizedClient Authorized Client} using the {@link OAuth2AuthorizedClientService}. + * and associated to the {@link OAuth2AuthorizedClient Authorized Client} using the {@link OAuth2AuthorizedClientRepository}. *
    • *
    • * Finally, the {@link OAuth2AuthenticationToken} is returned and ultimately stored @@ -88,7 +87,7 @@ * @see OAuth2AuthorizationRequestRedirectFilter * @see ClientRegistrationRepository * @see OAuth2AuthorizedClient - * @see OAuth2AuthorizedClientService + * @see OAuth2AuthorizedClientRepository * @see Section 4.1 Authorization Code Grant * @see Section 4.1.2 Authorization Response */ @@ -100,7 +99,7 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProce private static final String AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE = "authorization_request_not_found"; private static final String CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE = "client_registration_not_found"; private ClientRegistrationRepository clientRegistrationRepository; - private OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository authorizedClientRepository; private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); @@ -125,11 +124,26 @@ public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegist public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService, String filterProcessesUrl) { + this(clientRegistrationRepository, + new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService), filterProcessesUrl); + } + + /** + * Constructs an {@code OAuth2LoginAuthenticationFilter} using the provided parameters. + * + * @since 5.1 + * @param clientRegistrationRepository the repository of client registrations + * @param authorizedClientRepository the authorized client repository + * @param filterProcessesUrl the {@code URI} where this {@code Filter} will process the authentication requests + */ + public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository, + String filterProcessesUrl) { super(filterProcessesUrl); Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); - Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); + Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); this.clientRegistrationRepository = clientRegistrationRepository; - this.authorizedClientService = authorizedClientService; + this.authorizedClientRepository = authorizedClientRepository; } @Override @@ -142,7 +156,8 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } - OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request); + OAuth2AuthorizationRequest authorizationRequest = + this.authorizationRequestRepository.removeAuthorizationRequest(request, response); if (authorizationRequest == null) { OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); @@ -173,9 +188,10 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( authenticationResult.getClientRegistration(), oauth2Authentication.getName(), - authenticationResult.getAccessToken()); + authenticationResult.getAccessToken(), + authenticationResult.getRefreshToken()); - this.authorizedClientService.saveAuthorizedClient(authorizedClient, oauth2Authentication); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response); return oauth2Authentication; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverter.java index 80d62789ece..1011a41e7a9 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverter.java @@ -16,8 +16,6 @@ package org.springframework.security.oauth2.client.web; -import java.util.function.Function; - import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; @@ -27,6 +25,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; @@ -40,10 +39,9 @@ * converter does not validate any errors it only performs a conversion. * @author Rob Winch * @since 5.1 - * @see org.springframework.security.web.server.authentication.AuthenticationWebFilter#setAuthenticationConverter(Function) + * @see org.springframework.security.web.server.authentication.AuthenticationWebFilter#setAuthenticationConverter(ServerAuthenticationConverter) */ -public class ServerOAuth2LoginAuthenticationTokenConverter implements - Function> { +public class ServerOAuth2LoginAuthenticationTokenConverter implements ServerAuthenticationConverter { static final String AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE = "authorization_request_not_found"; @@ -72,7 +70,7 @@ public void setAuthorizationRequestRepository( } @Override - public Mono apply(ServerWebExchange serverWebExchange) { + public Mono convert(ServerWebExchange serverWebExchange) { return this.authorizationRequestRepository.removeAuthorizationRequest(serverWebExchange) .switchIfEmpty(oauth2AuthenticationException(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE)) .flatMap(authorizationRequest -> authenticationRequest(serverWebExchange, authorizationRequest)); @@ -97,14 +95,14 @@ private Mono authenticationRequest(ServerWebExch }) .switchIfEmpty(oauth2AuthenticationException(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE)) .map(clientRegistration -> { - OAuth2AuthorizationResponse authorizationResponse = convert(exchange); + OAuth2AuthorizationResponse authorizationResponse = convertToResponse(exchange); OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken( clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse)); return authenticationRequest; }); } - private static OAuth2AuthorizationResponse convert(ServerWebExchange exchange) { + private static OAuth2AuthorizationResponse convertToResponse(ServerWebExchange exchange) { MultiValueMap queryParams = exchange.getRequest() .getQueryParams(); String redirectUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepository.java index 2783c5968f3..aa8b4302a63 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepository.java @@ -84,7 +84,11 @@ public Mono removeAuthorizationRequest( if (stateToAuthzRequest.isEmpty()) { sessionAttrs.remove(this.sessionAttributeName); } - sink.next(removedValue); + if (removedValue == null) { + sink.complete(); + } else { + sink.next(removedValue); + } }); } 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 new file mode 100644 index 00000000000..91d7db916ba --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.method.annotation; + +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * An implementation of a {@link HandlerMethodArgumentResolver} that is capable + * of resolving a method parameter to an argument value of type {@link OAuth2AuthorizedClient}. + * + *

      + * For example: + *

      + * @Controller
      + * public class MyController {
      + *     @GetMapping("/authorized-client")
      + *     public String authorizedClient(@RegisteredOAuth2AuthorizedClient("login-client") OAuth2AuthorizedClient authorizedClient) {
      + *         // do something with authorizedClient
      + *     }
      + * }
      + * 
      + * + * @author Joe Grandja + * @since 5.1 + * @see RegisteredOAuth2AuthorizedClient + */ +public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMethodArgumentResolver { + private final ClientRegistrationRepository clientRegistrationRepository; + private final OAuth2AuthorizedClientRepository authorizedClientRepository; + private OAuth2AccessTokenResponseClient clientCredentialsTokenResponseClient = + new DefaultClientCredentialsTokenResponseClient(); + + /** + * Constructs an {@code OAuth2AuthorizedClientArgumentResolver} using the provided parameters. + * + * @param clientRegistrationRepository the repository of client registrations + * @param authorizedClientRepository the repository of authorized clients + */ + public OAuth2AuthorizedClientArgumentResolver(ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); + this.clientRegistrationRepository = clientRegistrationRepository; + this.authorizedClientRepository = authorizedClientRepository; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + Class parameterType = parameter.getParameterType(); + return (OAuth2AuthorizedClient.class.isAssignableFrom(parameterType) && + (AnnotatedElementUtils.findMergedAnnotation( + parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class) != null)); + } + + @NonNull + @Override + public Object resolveArgument(MethodParameter parameter, + @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + @Nullable WebDataBinderFactory binderFactory) throws Exception { + + String clientRegistrationId = this.resolveClientRegistrationId(parameter); + 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\")."); + } + + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + clientRegistrationId, principal, servletRequest); + if (authorizedClient != null) { + return authorizedClient; + } + + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId); + if (clientRegistration == null) { + return null; + } + + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { + throw new ClientAuthorizationRequiredException(clientRegistrationId); + } + + if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType())) { + HttpServletResponse servletResponse = webRequest.getNativeResponse(HttpServletResponse.class); + authorizedClient = this.authorizeClientCredentialsClient(clientRegistration, servletRequest, servletResponse); + } + + return authorizedClient; + } + + private String resolveClientRegistrationId(MethodParameter parameter) { + RegisteredOAuth2AuthorizedClient authorizedClientAnnotation = AnnotatedElementUtils.findMergedAnnotation( + parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class); + + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); + + String clientRegistrationId = null; + if (!StringUtils.isEmpty(authorizedClientAnnotation.registrationId())) { + clientRegistrationId = authorizedClientAnnotation.registrationId(); + } else if (!StringUtils.isEmpty(authorizedClientAnnotation.value())) { + clientRegistrationId = authorizedClientAnnotation.value(); + } else if (principal != null && OAuth2AuthenticationToken.class.isAssignableFrom(principal.getClass())) { + clientRegistrationId = ((OAuth2AuthenticationToken) principal).getAuthorizedClientRegistrationId(); + } + + return clientRegistrationId; + } + + private OAuth2AuthorizedClient authorizeClientCredentialsClient(ClientRegistration clientRegistration, + HttpServletRequest request, HttpServletResponse response) { + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(clientRegistration); + OAuth2AccessTokenResponse tokenResponse = + this.clientCredentialsTokenResponseClient.getTokenResponse(clientCredentialsGrantRequest); + + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); + + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + clientRegistration, + (principal != null ? principal.getName() : "anonymousUser"), + tokenResponse.getAccessToken()); + + this.authorizedClientRepository.saveAuthorizedClient( + authorizedClient, + principal, + request, + response); + + return authorizedClient; + } + + /** + * Sets the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant. + * + * @param clientCredentialsTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant + */ + public final void setClientCredentialsTokenResponseClient( + OAuth2AccessTokenResponseClient clientCredentialsTokenResponseClient) { + Assert.notNull(clientCredentialsTokenResponseClient, "clientCredentialsTokenResponseClient cannot be null"); + this.clientCredentialsTokenResponseClient = clientCredentialsTokenResponseClient; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolver.java deleted file mode 100644 index 1c0b99d95a9..00000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolver.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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.web.method.annotation; - -import org.springframework.core.MethodParameter; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.method.support.ModelAndViewContainer; - -/** - * An implementation of a {@link HandlerMethodArgumentResolver} that is capable - * of resolving a method parameter into an argument value for the following types: - * {@link ClientRegistration}, {@link OAuth2AuthorizedClient} and {@link OAuth2AccessToken}. - * - *

      - * For example: - *

      - * @Controller
      - * public class MyController {
      - *     @GetMapping("/client-registration")
      - *     public String clientRegistration(@OAuth2Client("login-client") ClientRegistration clientRegistration) {
      - *         // do something with clientRegistration
      - *     }
      - *
      - *     @GetMapping("/authorized-client")
      - *     public String authorizedClient(@OAuth2Client("login-client") OAuth2AuthorizedClient authorizedClient) {
      - *         // do something with authorizedClient
      - *     }
      - *
      - *     @GetMapping("/access-token")
      - *     public String accessToken(@OAuth2Client("login-client") OAuth2AccessToken accessToken) {
      - *         // do something with accessToken
      - *     }
      - * }
      - * 
      - * - * @author Joe Grandja - * @since 5.1 - * @see OAuth2Client - */ -public final class OAuth2ClientArgumentResolver implements HandlerMethodArgumentResolver { - private final ClientRegistrationRepository clientRegistrationRepository; - private final OAuth2AuthorizedClientService authorizedClientService; - - /** - * Constructs an {@code OAuth2ClientArgumentResolver} using the provided parameters. - * - * @param clientRegistrationRepository the repository of client registrations - * @param authorizedClientService the authorized client service - */ - public OAuth2ClientArgumentResolver(ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientService authorizedClientService) { - Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); - Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); - this.clientRegistrationRepository = clientRegistrationRepository; - this.authorizedClientService = authorizedClientService; - } - - @Override - public boolean supportsParameter(MethodParameter parameter) { - Class parameterType = parameter.getParameterType(); - return ((OAuth2AccessToken.class.isAssignableFrom(parameterType) || - OAuth2AuthorizedClient.class.isAssignableFrom(parameterType) || - ClientRegistration.class.isAssignableFrom(parameterType)) && - (parameter.hasParameterAnnotation(OAuth2Client.class))); - } - - @NonNull - @Override - public Object resolveArgument(MethodParameter parameter, - @Nullable ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, - @Nullable WebDataBinderFactory binderFactory) throws Exception { - - OAuth2Client oauth2ClientAnnotation = parameter.getParameterAnnotation(OAuth2Client.class); - Authentication principal = SecurityContextHolder.getContext().getAuthentication(); - - String clientRegistrationId = null; - if (!StringUtils.isEmpty(oauth2ClientAnnotation.registrationId())) { - clientRegistrationId = oauth2ClientAnnotation.registrationId(); - } else if (!StringUtils.isEmpty(oauth2ClientAnnotation.value())) { - clientRegistrationId = oauth2ClientAnnotation.value(); - } else if (principal != null && OAuth2AuthenticationToken.class.isAssignableFrom(principal.getClass())) { - clientRegistrationId = ((OAuth2AuthenticationToken) principal).getAuthorizedClientRegistrationId(); - } - if (StringUtils.isEmpty(clientRegistrationId)) { - throw new IllegalArgumentException("Unable to resolve the Client Registration Identifier. " + - "It must be provided via @OAuth2Client(\"client1\") or @OAuth2Client(registrationId = \"client1\")."); - } - - if (ClientRegistration.class.isAssignableFrom(parameter.getParameterType())) { - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId); - if (clientRegistration == null) { - throw new IllegalArgumentException("Unable to find ClientRegistration with registration identifier \"" + - clientRegistrationId + "\"."); - } - return clientRegistration; - } - - if (principal == null) { - // An Authentication is required given that an OAuth2AuthorizedClient is associated to a Principal - throw new IllegalStateException("Unable to resolve the Authorized Client with registration identifier \"" + - clientRegistrationId + "\". An \"authenticated\" or \"unauthenticated\" session is required. " + - "To allow for unauthenticated access, ensure HttpSecurity.anonymous() is configured."); - } - - OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient( - clientRegistrationId, principal.getName()); - if (authorizedClient == null) { - throw new ClientAuthorizationRequiredException(clientRegistrationId); - } - - return OAuth2AccessToken.class.isAssignableFrom(parameter.getParameterType()) ? - authorizedClient.getAccessToken() : authorizedClient; - } -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2ExchangeFilterFunctions.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2ExchangeFilterFunctions.java new file mode 100644 index 00000000000..fcf2f56008a --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/OAuth2ExchangeFilterFunctions.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.reactive.function.client; + +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.function.Consumer; + +/** + * @author Rob Winch + * @since 5.1 + */ +public final class OAuth2ExchangeFilterFunctions { + + /** + * Configures the WebClient for OAuth2 support in a servlet environment. + * @param repository the repository to use + * @return the {@link Consumer} to configure the WebClient + */ + public static Consumer oauth2ServletConfig(OAuth2AuthorizedClientRepository repository) { + return new ServletOAuth2AuthorizedClientExchangeFilterFunction(repository) + .oauth2Configuration(); + } + + /** + * Configures the WebClient for OAuth2 support in a servlet environment. + * @return the {@link Consumer} to configure the WebClient + */ + public static Consumer oauth2ServletConfig() { + return new ServletOAuth2AuthorizedClientExchangeFilterFunction() + .oauth2Configuration(); + } + + private OAuth2ExchangeFilterFunctions() {} +} 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 new file mode 100644 index 00000000000..1891dd4e3b5 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java @@ -0,0 +1,237 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.reactive.function.client; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.util.Assert; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse; + +/** + * Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the + * token as a Bearer Token. + * + * @author Rob Winch + * @since 5.1 + */ +public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction { + /** + * The request attribute name used to locate the {@link OAuth2AuthorizedClient}. + */ + private static final String OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME = OAuth2AuthorizedClient.class.getName(); + + private Clock clock = Clock.systemUTC(); + + private Duration accessTokenExpiresSkew = Duration.ofMinutes(1); + + private ReactiveOAuth2AuthorizedClientService authorizedClientService; + + public ServerOAuth2AuthorizedClientExchangeFilterFunction() {} + + public ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientService authorizedClientService) { + this.authorizedClientService = authorizedClientService; + } + + /** + * Modifies the {@link ClientRequest#attributes()} to include the {@link OAuth2AuthorizedClient} to be used for + * providing the Bearer Token. Example usage: + * + *
      +	 * WebClient webClient = WebClient.builder()
      +	 *    .filter(new OAuth2AuthorizedClientExchangeFilterFunction(authorizedClientService))
      +	 *    .build();
      +	 * Mono response = webClient
      +	 *    .get()
      +	 *    .uri(uri)
      +	 *    .attributes(oauth2AuthorizedClient(authorizedClient))
      +	 *    // ...
      +	 *    .retrieve()
      +	 *    .bodyToMono(String.class);
      +	 * 
      + * + * An attempt to automatically refresh the token will be made if all of the following + * are true: + * + *
        + *
      • The ReactiveOAuth2AuthorizedClientService on the + * {@link ServerOAuth2AuthorizedClientExchangeFilterFunction} is not null
      • + *
      • A refresh token is present on the OAuth2AuthorizedClient
      • + *
      • The access token will be expired in + * {@link #setAccessTokenExpiresSkew(Duration)}
      • + *
      • The {@link ReactiveSecurityContextHolder} will be used to attempt to save + * the token. If it is empty, then the principal name on the OAuth2AuthorizedClient + * will be used to create an Authentication for saving.
      • + *
      + * + * @param authorizedClient the {@link OAuth2AuthorizedClient} to use. + * @return the {@link Consumer} to populate the + */ + public static Consumer> oauth2AuthorizedClient(OAuth2AuthorizedClient authorizedClient) { + return attributes -> attributes.put(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME, authorizedClient); + } + + /** + * An access token will be considered expired by comparing its expiration to now + + * this skewed Duration. The default is 1 minute. + * @param accessTokenExpiresSkew the Duration to use. + */ + public void setAccessTokenExpiresSkew(Duration accessTokenExpiresSkew) { + Assert.notNull(accessTokenExpiresSkew, "accessTokenExpiresSkew cannot be null"); + this.accessTokenExpiresSkew = accessTokenExpiresSkew; + } + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + Optional attribute = request.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME) + .map(OAuth2AuthorizedClient.class::cast); + return Mono.justOrEmpty(attribute) + .flatMap(authorizedClient -> authorizedClient(next, authorizedClient)) + .map(authorizedClient -> bearer(request, authorizedClient)) + .flatMap(next::exchange) + .switchIfEmpty(next.exchange(request)); + } + + private Mono authorizedClient(ExchangeFunction next, OAuth2AuthorizedClient authorizedClient) { + if (shouldRefresh(authorizedClient)) { + return refreshAuthorizedClient(next, authorizedClient); + } + return Mono.just(authorizedClient); + } + + private Mono refreshAuthorizedClient(ExchangeFunction next, + OAuth2AuthorizedClient authorizedClient) { + ClientRegistration clientRegistration = authorizedClient + .getClientRegistration(); + String tokenUri = clientRegistration + .getProviderDetails().getTokenUri(); + ClientRequest request = ClientRequest.create(HttpMethod.POST, URI.create(tokenUri)) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret())) + .body(refreshTokenBody(authorizedClient.getRefreshToken().getTokenValue())) + .build(); + return next.exchange(request) + .flatMap(response -> response.body(oauth2AccessTokenResponse())) + .map(accessTokenResponse -> new OAuth2AuthorizedClient(authorizedClient.getClientRegistration(), authorizedClient.getPrincipalName(), accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken())) + .flatMap(result -> ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .defaultIfEmpty(new PrincipalNameAuthentication(authorizedClient.getPrincipalName())) + .flatMap(principal -> this.authorizedClientService.saveAuthorizedClient(result, principal)) + .thenReturn(result)); + } + + private boolean shouldRefresh(OAuth2AuthorizedClient authorizedClient) { + if (this.authorizedClientService == null) { + return false; + } + OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken(); + if (refreshToken == null) { + return false; + } + Instant now = this.clock.instant(); + Instant expiresAt = authorizedClient.getAccessToken().getExpiresAt(); + if (now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew))) { + return true; + } + return false; + } + + private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient authorizedClient) { + return ClientRequest.from(request) + .headers(headers -> headers.setBearerAuth(authorizedClient.getAccessToken().getTokenValue())) + .build(); + } + + private static BodyInserters.FormInserter refreshTokenBody(String refreshToken) { + return BodyInserters + .fromFormData("grant_type", AuthorizationGrantType.REFRESH_TOKEN.getValue()) + .with("refresh_token", refreshToken); + } + + private static class PrincipalNameAuthentication implements Authentication { + private final String username; + + private PrincipalNameAuthentication(String username) { + this.username = username; + } + + @Override + public Collection getAuthorities() { + throw unsupported(); + } + + @Override + public Object getCredentials() { + throw unsupported(); + } + + @Override + public Object getDetails() { + throw unsupported(); + } + + @Override + public Object getPrincipal() { + throw unsupported(); + } + + @Override + public boolean isAuthenticated() { + throw unsupported(); + } + + @Override + public void setAuthenticated(boolean isAuthenticated) + throws IllegalArgumentException { + throw unsupported(); + } + + @Override + public String getName() { + return this.username; + } + + private UnsupportedOperationException unsupported() { + return new UnsupportedOperationException("Not Supported"); + } + } +} 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 new file mode 100644 index 00000000000..65d6c26c4b8 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java @@ -0,0 +1,421 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.reactive.function.client; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.util.Assert; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.URI; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse; + +/** + * Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth2 requests by including the + * token as a Bearer Token. It also provides mechanisms for looking up the {@link OAuth2AuthorizedClient}. This class is + * intended to be used in a servlet environment. + * + * Example usage: + * + *
      + * OAuth2AuthorizedClientExchangeFilterFunction oauth2 = new OAuth2AuthorizedClientExchangeFilterFunction(authorizedClientService);
      + * WebClient webClient = WebClient.builder()
      + *    .apply(oauth2.oauth2Configuration())
      + *    .build();
      + * Mono response = webClient
      + *    .get()
      + *    .uri(uri)
      + *    .attributes(oauth2AuthorizedClient(authorizedClient))
      + *    // ...
      + *    .retrieve()
      + *    .bodyToMono(String.class);
      + * 
      + * + * An attempt to automatically refresh the token will be made if all of the following + * are true: + * + *
        + *
      • The ReactiveOAuth2AuthorizedClientService on the + * {@link ServletOAuth2AuthorizedClientExchangeFilterFunction} is not null
      • + *
      • A refresh token is present on the OAuth2AuthorizedClient
      • + *
      • The access token will be expired in + * {@link #setAccessTokenExpiresSkew(Duration)}
      • + *
      • The {@link ReactiveSecurityContextHolder} will be used to attempt to save + * the token. If it is empty, then the principal name on the OAuth2AuthorizedClient + * will be used to create an Authentication for saving.
      • + *
      + * + * @author Rob Winch + * @since 5.1 + */ +public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction { + /** + * The request attribute name used to locate the {@link OAuth2AuthorizedClient}. + */ + private static final String OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME = OAuth2AuthorizedClient.class.getName(); + private static final String CLIENT_REGISTRATION_ID_ATTR_NAME = OAuth2AuthorizedClient.class.getName().concat(".CLIENT_REGISTRATION_ID"); + private static final String AUTHENTICATION_ATTR_NAME = Authentication.class.getName(); + private static final String HTTP_SERVLET_REQUEST_ATTR_NAME = HttpServletRequest.class.getName(); + private static final String HTTP_SERVLET_RESPONSE_ATTR_NAME = HttpServletResponse.class.getName(); + + private Clock clock = Clock.systemUTC(); + + private Duration accessTokenExpiresSkew = Duration.ofMinutes(1); + + private OAuth2AuthorizedClientRepository authorizedClientRepository; + + private boolean defaultOAuth2AuthorizedClient; + + public ServletOAuth2AuthorizedClientExchangeFilterFunction() {} + + public ServletOAuth2AuthorizedClientExchangeFilterFunction(OAuth2AuthorizedClientRepository authorizedClientRepository) { + this.authorizedClientRepository = authorizedClientRepository; + } + + /** + * If true, a default {@link OAuth2AuthorizedClient} can be discovered from the current Authentication. It is + * recommended to be cautious with this feature since all HTTP requests will receive the access token if it can be + * resolved from the current Authentication. + * @param defaultOAuth2AuthorizedClient true if a default {@link OAuth2AuthorizedClient} should be used, else false. + * Default is false. + */ + public void setDefaultOAuth2AuthorizedClient(boolean defaultOAuth2AuthorizedClient) { + this.defaultOAuth2AuthorizedClient = defaultOAuth2AuthorizedClient; + } + + /** + * Configures the builder with {@link #defaultRequest()} and adds this as a {@link ExchangeFilterFunction} + * @return the {@link Consumer} to configure the builder + */ + public Consumer oauth2Configuration() { + return builder -> builder.defaultRequest(defaultRequest()).filter(this); + } + + /** + * Provides defaults for the {@link HttpServletRequest} and the {@link HttpServletResponse} using + * {@link RequestContextHolder}. It also provides defaults for the {@link Authentication} using + * {@link SecurityContextHolder}. It also can default the {@link OAuth2AuthorizedClient} using the + * {@link #clientRegistrationId(String)} or the {@link #authentication(Authentication)}. + * @return the {@link Consumer} to populate the attributes + */ + public Consumer> defaultRequest() { + return spec -> { + spec.attributes(attrs -> { + populateDefaultRequestResponse(attrs); + populateDefaultAuthentication(attrs); + populateDefaultOAuth2AuthorizedClient(attrs); + }); + }; + } + + /** + * Modifies the {@link ClientRequest#attributes()} to include the {@link OAuth2AuthorizedClient} to be used for + * providing the Bearer Token. + * + * @param authorizedClient the {@link OAuth2AuthorizedClient} to use. + * @return the {@link Consumer} to populate the attributes + */ + public static Consumer> oauth2AuthorizedClient(OAuth2AuthorizedClient authorizedClient) { + return attributes -> { + if (authorizedClient == null) { + attributes.remove(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME); + } else { + attributes.put(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME, authorizedClient); + } + }; + } + + /** + * Modifies the {@link ClientRequest#attributes()} to include the {@link ClientRegistration#getRegistrationId()} to + * be used to look up the {@link OAuth2AuthorizedClient}. + * + * @param clientRegistrationId the {@link ClientRegistration#getRegistrationId()} to + * be used to look up the {@link OAuth2AuthorizedClient}. + * @return the {@link Consumer} to populate the attributes + */ + public static Consumer> clientRegistrationId(String clientRegistrationId) { + return attributes -> attributes.put(CLIENT_REGISTRATION_ID_ATTR_NAME, clientRegistrationId); + } + + /** + * Modifies the {@link ClientRequest#attributes()} to include the {@link Authentication} used to + * look up and save the {@link OAuth2AuthorizedClient}. The value is defaulted in + * {@link ServletOAuth2AuthorizedClientExchangeFilterFunction#defaultRequest()} + * + * @param authentication the {@link Authentication} to use. + * @return the {@link Consumer} to populate the attributes + */ + public static Consumer> authentication(Authentication authentication) { + return attributes -> attributes.put(AUTHENTICATION_ATTR_NAME, authentication); + } + + /** + * Modifies the {@link ClientRequest#attributes()} to include the {@link HttpServletRequest} used to + * look up and save the {@link OAuth2AuthorizedClient}. The value is defaulted in + * {@link ServletOAuth2AuthorizedClientExchangeFilterFunction#defaultRequest()} + * + * @param request the {@link HttpServletRequest} to use. + * @return the {@link Consumer} to populate the attributes + */ + public static Consumer> httpServletRequest(HttpServletRequest request) { + return attributes -> attributes.put(HTTP_SERVLET_REQUEST_ATTR_NAME, request); + } + + /** + * Modifies the {@link ClientRequest#attributes()} to include the {@link HttpServletResponse} used to + * save the {@link OAuth2AuthorizedClient}. The value is defaulted in + * {@link ServletOAuth2AuthorizedClientExchangeFilterFunction#defaultRequest()} + * + * @param response the {@link HttpServletResponse} to use. + * @return the {@link Consumer} to populate the attributes + */ + public static Consumer> httpServletResponse(HttpServletResponse response) { + return attributes -> attributes.put(HTTP_SERVLET_RESPONSE_ATTR_NAME, response); + } + + /** + * An access token will be considered expired by comparing its expiration to now + + * this skewed Duration. The default is 1 minute. + * @param accessTokenExpiresSkew the Duration to use. + */ + public void setAccessTokenExpiresSkew(Duration accessTokenExpiresSkew) { + Assert.notNull(accessTokenExpiresSkew, "accessTokenExpiresSkew cannot be null"); + this.accessTokenExpiresSkew = accessTokenExpiresSkew; + } + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + Optional attribute = request.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME) + .map(OAuth2AuthorizedClient.class::cast); + return Mono.justOrEmpty(attribute) + .flatMap(authorizedClient -> authorizedClient(request, next, authorizedClient)) + .map(authorizedClient -> bearer(request, authorizedClient)) + .flatMap(next::exchange) + .switchIfEmpty(next.exchange(request)); + } + + private void populateDefaultRequestResponse(Map attrs) { + if (attrs.containsKey(HTTP_SERVLET_REQUEST_ATTR_NAME) && attrs.containsKey( + HTTP_SERVLET_RESPONSE_ATTR_NAME)) { + return; + } + ServletRequestAttributes context = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = null; + HttpServletResponse response = null; + if (context != null) { + request = context.getRequest(); + response = context.getResponse(); + } + attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, request); + attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, response); + } + + private void populateDefaultAuthentication(Map attrs) { + if (attrs.containsKey(AUTHENTICATION_ATTR_NAME)) { + return; + } + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + attrs.putIfAbsent(AUTHENTICATION_ATTR_NAME, authentication); + } + + private void populateDefaultOAuth2AuthorizedClient(Map attrs) { + if (this.authorizedClientRepository == null + || attrs.containsKey(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME)) { + return; + } + + Authentication authentication = getAuthentication(attrs); + String clientRegistrationId = getClientRegistrationId(attrs); + if (clientRegistrationId == null + && this.defaultOAuth2AuthorizedClient + && authentication instanceof OAuth2AuthenticationToken) { + clientRegistrationId = ((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(); + } + if (clientRegistrationId != null) { + HttpServletRequest request = (HttpServletRequest) attrs.get( + HTTP_SERVLET_REQUEST_ATTR_NAME); + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository + .loadAuthorizedClient(clientRegistrationId, authentication, + request); + if (authorizedClient == null) { + throw new ClientAuthorizationRequiredException(clientRegistrationId); + } + oauth2AuthorizedClient(authorizedClient).accept(attrs); + } + } + + private Mono authorizedClient(ClientRequest request, ExchangeFunction next, OAuth2AuthorizedClient authorizedClient) { + if (shouldRefresh(authorizedClient)) { + return refreshAuthorizedClient(request, next, authorizedClient); + } + return Mono.just(authorizedClient); + } + + private Mono refreshAuthorizedClient(ClientRequest request, ExchangeFunction next, + OAuth2AuthorizedClient authorizedClient) { + ClientRegistration clientRegistration = authorizedClient + .getClientRegistration(); + String tokenUri = clientRegistration + .getProviderDetails().getTokenUri(); + ClientRequest refreshRequest = ClientRequest.create(HttpMethod.POST, URI.create(tokenUri)) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret())) + .body(refreshTokenBody(authorizedClient.getRefreshToken().getTokenValue())) + .build(); + return next.exchange(refreshRequest) + .flatMap(response -> response.body(oauth2AccessTokenResponse())) + .map(accessTokenResponse -> new OAuth2AuthorizedClient(authorizedClient.getClientRegistration(), authorizedClient.getPrincipalName(), accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken())) + .map(result -> { + Authentication principal = (Authentication) request.attribute( + AUTHENTICATION_ATTR_NAME).orElse(new PrincipalNameAuthentication(authorizedClient.getPrincipalName())); + HttpServletRequest httpRequest = (HttpServletRequest) request.attributes().get( + HTTP_SERVLET_REQUEST_ATTR_NAME); + HttpServletResponse httpResponse = (HttpServletResponse) request.attributes().get( + HTTP_SERVLET_RESPONSE_ATTR_NAME); + this.authorizedClientRepository.saveAuthorizedClient(result, principal, httpRequest, httpResponse); + return result; + }) + .publishOn(Schedulers.elastic()); + } + + private boolean shouldRefresh(OAuth2AuthorizedClient authorizedClient) { + if (this.authorizedClientRepository == null) { + return false; + } + OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken(); + if (refreshToken == null) { + return false; + } + Instant now = this.clock.instant(); + Instant expiresAt = authorizedClient.getAccessToken().getExpiresAt(); + if (now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew))) { + return true; + } + return false; + } + + private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient authorizedClient) { + return ClientRequest.from(request) + .headers(headers -> headers.setBearerAuth(authorizedClient.getAccessToken().getTokenValue())) + .build(); + } + + private static BodyInserters.FormInserter refreshTokenBody(String refreshToken) { + return BodyInserters + .fromFormData("grant_type", AuthorizationGrantType.REFRESH_TOKEN.getValue()) + .with("refresh_token", refreshToken); + } + + static OAuth2AuthorizedClient getOAuth2AuthorizedClient(Map attrs) { + return (OAuth2AuthorizedClient) attrs.get(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME); + } + + static String getClientRegistrationId(Map attrs) { + return (String) attrs.get(CLIENT_REGISTRATION_ID_ATTR_NAME); + } + + static Authentication getAuthentication(Map attrs) { + return (Authentication) attrs.get(AUTHENTICATION_ATTR_NAME); + } + + static HttpServletRequest getRequest(Map attrs) { + return (HttpServletRequest) attrs.get(HTTP_SERVLET_REQUEST_ATTR_NAME); + } + + static HttpServletResponse getResponse(Map attrs) { + return (HttpServletResponse) attrs.get(HTTP_SERVLET_RESPONSE_ATTR_NAME); + } + + private static class PrincipalNameAuthentication implements Authentication { + private final String username; + + private PrincipalNameAuthentication(String username) { + this.username = username; + } + + @Override + public Collection getAuthorities() { + throw unsupported(); + } + + @Override + public Object getCredentials() { + throw unsupported(); + } + + @Override + public Object getDetails() { + throw unsupported(); + } + + @Override + public Object getPrincipal() { + throw unsupported(); + } + + @Override + public boolean isAuthenticated() { + throw unsupported(); + } + + @Override + public void setAuthenticated(boolean isAuthenticated) + throws IllegalArgumentException { + throw unsupported(); + } + + @Override + public String getName() { + return this.username; + } + + private UnsupportedOperationException unsupported() { + return new UnsupportedOperationException("Not Supported"); + } + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2ClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java similarity index 61% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2ClientArgumentResolver.java rename to oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java index 4be413668a4..6b679cc6799 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2ClientArgumentResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java @@ -24,92 +24,65 @@ import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; - import reactor.core.publisher.Mono; /** * An implementation of a {@link HandlerMethodArgumentResolver} that is capable - * of resolving a method parameter into an argument value for the following types: - * {@link ClientRegistration}, {@link OAuth2AuthorizedClient} and {@link OAuth2AccessToken}. + * of resolving a method parameter to an argument value of type {@link OAuth2AuthorizedClient}. * *

      * For example: *

        * @Controller
        * public class MyController {
      - *     @GetMapping("/client-registration")
      - *     public Mono clientRegistration(@OAuth2Client("login-client") ClientRegistration clientRegistration) {
      - *         // do something with clientRegistration
      - *     }
      - *
        *     @GetMapping("/authorized-client")
      - *     public Mono authorizedClient(@OAuth2Client("login-client") OAuth2AuthorizedClient authorizedClient) {
      + *     public Mono<String> authorizedClient(@RegisteredOAuth2AuthorizedClient("login-client") OAuth2AuthorizedClient authorizedClient) {
        *         // do something with authorizedClient
        *     }
      - *
      - *     @GetMapping("/access-token")
      - *     public Mono accessToken(@OAuth2Client("login-client") OAuth2AccessToken accessToken) {
      - *         // do something with accessToken
      - *     }
        * }
        * 
      * * @author Rob Winch * @since 5.1 - * @see OAuth2Client + * @see RegisteredOAuth2AuthorizedClient */ -public final class OAuth2ClientArgumentResolver implements HandlerMethodArgumentResolver { - private final ReactiveClientRegistrationRepository clientRegistrationRepository; +public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMethodArgumentResolver { private final ReactiveOAuth2AuthorizedClientService authorizedClientService; /** - * Constructs an {@code OAuth2ClientArgumentResolver} using the provided parameters. + * Constructs an {@code OAuth2AuthorizedClientArgumentResolver} using the provided parameters. * - * @param clientRegistrationRepository the repository of client registrations * @param authorizedClientService the authorized client service */ - public OAuth2ClientArgumentResolver(ReactiveClientRegistrationRepository clientRegistrationRepository, - ReactiveOAuth2AuthorizedClientService authorizedClientService) { - Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + public OAuth2AuthorizedClientArgumentResolver(ReactiveOAuth2AuthorizedClientService authorizedClientService) { Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); - this.clientRegistrationRepository = clientRegistrationRepository; this.authorizedClientService = authorizedClientService; } @Override public boolean supportsParameter(MethodParameter parameter) { - return AnnotatedElementUtils.findMergedAnnotation(parameter.getParameter(), OAuth2Client.class) != null; + return AnnotatedElementUtils.findMergedAnnotation(parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class) != null; } @Override public Mono resolveArgument( MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) { return Mono.defer(() -> { - OAuth2Client oauth2ClientAnnotation = AnnotatedElementUtils - .findMergedAnnotation(parameter.getParameter(), OAuth2Client.class); + RegisteredOAuth2AuthorizedClient authorizedClientAnnotation = AnnotatedElementUtils + .findMergedAnnotation(parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class); - Mono clientRegistrationId = Mono.justOrEmpty(oauth2ClientAnnotation.registrationId()) + Mono clientRegistrationId = Mono.justOrEmpty(authorizedClientAnnotation.registrationId()) .filter(id -> !StringUtils.isEmpty(id)) .switchIfEmpty(clientRegistrationId()) .switchIfEmpty(Mono.defer(() -> Mono.error(new IllegalArgumentException( - "Unable to resolve the Client Registration Identifier. It must be provided via @OAuth2Client(\"client1\") or @OAuth2Client(registrationId = \"client1\").")))); - - if (ClientRegistration.class.isAssignableFrom(parameter.getParameterType())) { - return clientRegistrationId.flatMap(id -> this.clientRegistrationRepository.findByRegistrationId(id) - .switchIfEmpty(Mono.defer(() -> Mono.error(new IllegalArgumentException( - "Unable to find ClientRegistration with registration identifier \"" - + id + "\"."))))).cast(Object.class); - } + "Unable to resolve the Client Registration Identifier. It must be provided via @RegisteredOAuth2AuthorizedClient(\"client1\") or @RegisteredOAuth2AuthorizedClient(registrationId = \"client1\").")))); Mono principalName = ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication).map(Authentication::getName); @@ -129,10 +102,6 @@ public Mono resolveArgument( registrationId)))); }).cast(OAuth2AuthorizedClient.class); - if (OAuth2AccessToken.class.isAssignableFrom(parameter.getParameterType())) { - return authorizedClient.map(OAuth2AuthorizedClient::getAccessToken); - } - return authorizedClient.cast(Object.class); }); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java index ba88b9f570a..85176b0d081 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java @@ -27,6 +27,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; @@ -122,8 +123,10 @@ public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizatio @Test public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessToken() { OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); + OAuth2RefreshToken refreshToken = mock(OAuth2RefreshToken.class); OAuth2AccessTokenResponse accessTokenResponse = mock(OAuth2AccessTokenResponse.class); when(accessTokenResponse.getAccessToken()).thenReturn(accessToken); + when(accessTokenResponse.getRefreshToken()).thenReturn(refreshToken); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); OAuth2AuthorizationCodeAuthenticationToken authenticationResult = @@ -137,5 +140,6 @@ public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessTo assertThat(authenticationResult.getClientRegistration()).isEqualTo(this.clientRegistration); assertThat(authenticationResult.getAuthorizationExchange()).isEqualTo(this.authorizationExchange); assertThat(authenticationResult.getAccessToken()).isEqualTo(accessToken); + assertThat(authenticationResult.getRefreshToken()).isEqualTo(refreshToken); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java index 63c54dbf33c..69949edb672 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -20,6 +20,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; @@ -41,10 +42,14 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.security.oauth2.core.user.OAuth2User; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.containsString; @@ -163,9 +168,7 @@ public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizatio @Test public void authenticateWhenLoginSuccessThenReturnAuthentication() { - OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); - OAuth2AccessTokenResponse accessTokenResponse = mock(OAuth2AccessTokenResponse.class); - when(accessTokenResponse.getAccessToken()).thenReturn(accessToken); + OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse(); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); OAuth2User principal = mock(OAuth2User.class); @@ -184,14 +187,13 @@ public void authenticateWhenLoginSuccessThenReturnAuthentication() { assertThat(authentication.getAuthorities()).isEqualTo(authorities); assertThat(authentication.getClientRegistration()).isEqualTo(this.clientRegistration); assertThat(authentication.getAuthorizationExchange()).isEqualTo(this.authorizationExchange); - assertThat(authentication.getAccessToken()).isEqualTo(accessToken); + assertThat(authentication.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + assertThat(authentication.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); } @Test public void authenticateWhenAuthoritiesMapperSetThenReturnMappedAuthorities() { - OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); - OAuth2AccessTokenResponse accessTokenResponse = mock(OAuth2AccessTokenResponse.class); - when(accessTokenResponse.getAccessToken()).thenReturn(accessToken); + OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse(); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); OAuth2User principal = mock(OAuth2User.class); @@ -212,4 +214,42 @@ public void authenticateWhenAuthoritiesMapperSetThenReturnMappedAuthorities() { assertThat(authentication.getAuthorities()).isEqualTo(mappedAuthorities); } + + // gh-5368 + @Test + public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToUserRequest() { + OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenSuccessResponse(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2User principal = mock(OAuth2User.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + when(principal.getAuthorities()).thenAnswer( + (Answer>) invocation -> authorities); + ArgumentCaptor userRequestArgCaptor = ArgumentCaptor.forClass(OAuth2UserRequest.class); + when(this.userService.loadUser(userRequestArgCaptor.capture())).thenReturn(principal); + + this.authenticationProvider.authenticate( + new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange)); + + assertThat(userRequestArgCaptor.getValue().getAdditionalParameters()).containsAllEntriesOf( + accessTokenResponse.getAdditionalParameters()); + } + + private OAuth2AccessTokenResponse accessTokenSuccessResponse() { + Instant expiresAt = Instant.now().plusSeconds(5); + Set scopes = new LinkedHashSet<>(Arrays.asList("scope1", "scope2")); + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + + return OAuth2AccessTokenResponse + .withToken("access-token-1234") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(expiresAt.getEpochSecond()) + .scopes(scopes) + .refreshToken("refresh-token-1234") + .additionalParameters(additionalParameters) + .build(); + + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManagerTests.java index cbc24b4606a..073d34230d7 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManagerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManagerTests.java @@ -23,11 +23,14 @@ import static org.mockito.Mockito.when; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -164,7 +167,7 @@ public void authenticationWhenOAuth2UserNotFoundThenEmpty() { } @Test - public void authenticationWhenOAuth2UserNotFoundThenSuccess() { + public void authenticationWhenOAuth2UserFoundThenSuccess() { OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") .tokenType(OAuth2AccessToken.TokenType.BEARER) .build(); @@ -179,6 +182,27 @@ public void authenticationWhenOAuth2UserNotFoundThenSuccess() { assertThat(result.isAuthenticated()).isTrue(); } + // gh-5368 + @Test + public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToUserRequest() { + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .additionalParameters(additionalParameters) + .build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); + DefaultOAuth2User user = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), Collections.singletonMap("user", "rob"), "user"); + ArgumentCaptor userRequestArgCaptor = ArgumentCaptor.forClass(OAuth2UserRequest.class); + when(this.userService.loadUser(userRequestArgCaptor.capture())).thenReturn(Mono.just(user)); + + this.manager.authenticate(loginToken()).block(); + + assertThat(userRequestArgCaptor.getValue().getAdditionalParameters()) + .containsAllEntriesOf(accessTokenResponse.getAdditionalParameters()); + } + private OAuth2LoginAuthenticationToken loginToken() { ClientRegistration clientRegistration = this.registration.build(); OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest 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 new file mode 100644 index 00000000000..117a17cb340 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java @@ -0,0 +1,326 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.endpoint; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; + +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link DefaultClientCredentialsTokenResponseClient}. + * + * @author Joe Grandja + */ +public class DefaultClientCredentialsTokenResponseClientTests { + private DefaultClientCredentialsTokenResponseClient tokenResponseClient = new DefaultClientCredentialsTokenResponseClient(); + private ClientRegistration clientRegistration; + private MockWebServer server; + + @Before + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + + String tokenUri = this.server.url("/oauth2/token").toString(); + + this.clientRegistration = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope("read", "write") + .tokenUri(tokenUri) + .build(); + } + + @After + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void setRestOperationsWhenRestOperationsIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.tokenResponseClient.setRestOperations(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void getTokenResponseWhenRequestIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() throws Exception { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\",\n" + + " \"scope\": \"read write\",\n" + + " \"custom_parameter_1\": \"custom-value-1\",\n" + + " \"custom_parameter_2\": \"custom-value-2\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + Instant expiresAtBefore = Instant.now().plusSeconds(3600); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest); + + Instant expiresAtAfter = Instant.now().plusSeconds(3600); + + RecordedRequest recordedRequest = this.server.takeRequest(); + assertThat(recordedRequest.getMethod()).isEqualTo(HttpMethod.POST.toString()); + assertThat(recordedRequest.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON.toString()); + assertThat(recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE)).startsWith(MediaType.APPLICATION_FORM_URLENCODED.toString()); + + String formParameters = recordedRequest.getBody().readUtf8(); + assertThat(formParameters).contains("grant_type=client_credentials"); + assertThat(formParameters).contains("scope=read+write"); + + assertThat(accessTokenResponse.getAccessToken().getTokenValue()).isEqualTo("access-token-1234"); + assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); + assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBetween(expiresAtBefore, expiresAtAfter); + assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write"); + assertThat(accessTokenResponse.getRefreshToken()).isNull(); + 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"); + } + + @Test + public void getTokenResponseWhenClientAuthenticationBasicThenAuthorizationHeaderIsSent() throws Exception { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest); + + RecordedRequest recordedRequest = this.server.takeRequest(); + assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).startsWith("Basic "); + } + + @Test + public void getTokenResponseWhenClientAuthenticationPostThenFormParametersAreSent() throws Exception { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + ClientRegistration clientRegistration = this.from(this.clientRegistration) + .clientAuthenticationMethod(ClientAuthenticationMethod.POST) + .build(); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(clientRegistration); + + this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest); + + RecordedRequest recordedRequest = this.server.takeRequest(); + assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull(); + + String formParameters = recordedRequest.getBody().readUtf8(); + assertThat(formParameters).contains("client_id=client-1"); + assertThat(formParameters).contains("client_secret=secret"); + } + + @Test + public void getTokenResponseWhenSuccessResponseAndNotBearerTokenTypeThenThrowOAuth2AuthenticationException() { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"not-bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[invalid_token_response] An error occurred parsing the Access Token response (200 OK): tokenType cannot be null"); + } + + @Test + public void getTokenResponseWhenSuccessResponseAndMissingTokenTypeParameterThenThrowOAuth2AuthenticationException() { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[invalid_token_response] An error occurred parsing the Access Token response (200 OK). Missing required parameters: access_token and/or token_type"); + } + + @Test + public void getTokenResponseWhenSuccessResponseIncludesScopeThenAccessTokenHasResponseScope() { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\",\n" + + " \"scope\": \"read\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest); + + assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read"); + } + + @Test + public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenAccessTokenHasDefaultScope() { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest); + + assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write"); + } + + @Test + public void getTokenResponseWhenTokenUriMalformedThenThrowOAuth2AuthenticationException() { + String malformedTokenUri = "http:\\provider.com\\oauth2\\token"; + ClientRegistration clientRegistration = this.from(this.clientRegistration) + .tokenUri(malformedTokenUri) + .build(); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[invalid_token_request] An error occurred while sending the Access Token Request:"); + } + + @Test + public void getTokenResponseWhenTokenUriInvalidThenThrowOAuth2AuthenticationException() { + String invalidTokenUri = "http://invalid-provider.com/oauth2/token"; + ClientRegistration clientRegistration = this.from(this.clientRegistration) + .tokenUri(invalidTokenUri) + .build(); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[invalid_token_request] An error occurred while sending the Access Token Request:"); + } + + @Test + public void getTokenResponseWhenMalformedResponseThenThrowOAuth2AuthenticationException() { + String accessTokenSuccessResponse = "{\n" + + " \"access_token\": \"access-token-1234\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"expires_in\": \"3600\",\n" + + " \"scope\": \"read write\",\n" + + " \"custom_parameter_1\": \"custom-value-1\",\n" + + " \"custom_parameter_2\": \"custom-value-2\"\n"; +// "}\n"; // Make the JSON invalid/malformed + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[invalid_token_request] An error occurred while sending the Access Token Request:"); + } + + @Test + public void getTokenResponseWhenErrorResponseThenThrowOAuth2AuthenticationException() { + String accessTokenErrorResponse = "{\n" + + " \"error\": \"unauthorized_client\"\n" + + "}\n"; + this.server.enqueue(jsonResponse(accessTokenErrorResponse).setResponseCode(400)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[unauthorized_client]"); + } + + @Test + public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthenticationException() { + this.server.enqueue(new MockResponse().setResponseCode(500)); + + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("[server_error]"); + } + + private MockResponse jsonResponse(String json) { + return new MockResponse() + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .setBody(json); + } + + private ClientRegistration.Builder from(ClientRegistration registration) { + return ClientRegistration.withRegistrationId(registration.getRegistrationId()) + .clientId(registration.getClientId()) + .clientSecret(registration.getClientSecret()) + .clientAuthenticationMethod(registration.getClientAuthenticationMethod()) + .authorizationGrantType(registration.getAuthorizationGrantType()) + .scope(registration.getScopes()) + .tokenUri(registration.getProviderDetails().getTokenUri()); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClientTests.java index 8c63eecb1a0..807fbb3c959 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusAuthorizationCodeTokenResponseClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -91,6 +91,7 @@ public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() t " \"token_type\": \"bearer\",\n" + " \"expires_in\": \"3600\",\n" + " \"scope\": \"openid profile\",\n" + + " \"refresh_token\": \"refresh-token-1234\",\n" + " \"custom_parameter_1\": \"custom-value-1\",\n" + " \"custom_parameter_2\": \"custom-value-2\"\n" + "}\n"; @@ -115,6 +116,7 @@ public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() t assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); 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().size()).isEqualTo(2); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_1", "custom-value-1"); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_2", "custom-value-2"); @@ -212,6 +214,28 @@ public void getTokenResponseWhenErrorResponseThenThrowOAuth2AuthenticationExcept } } + // gh-5594 + @Test + public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthenticationException() throws Exception { + this.exception.expect(OAuth2AuthenticationException.class); + this.exception.expectMessage(containsString("server_error")); + + MockWebServer server = new MockWebServer(); + + server.enqueue(new MockResponse().setResponseCode(500)); + server.start(); + + String tokenUri = server.url("/oauth2/token").toString(); + when(this.providerDetails.getTokenUri()).thenReturn(tokenUri); + + try { + this.tokenResponseClient.getTokenResponse( + new OAuth2AuthorizationCodeGrantRequest(this.clientRegistration, this.authorizationExchange)); + } finally { + server.shutdown(); + } + } + @Test public void getTokenResponseWhenSuccessResponseAndNotBearerTokenTypeThenThrowOAuth2AuthenticationException() throws Exception { this.exception.expect(OAuth2AuthenticationException.class); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequestTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequestTests.java new file mode 100644 index 00000000000..47e90132482 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/OAuth2ClientCredentialsGrantRequestTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.endpoint; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Java6Assertions.assertThatThrownBy; + +/** + * Tests for {@link OAuth2ClientCredentialsGrantRequest}. + * + * @author Joe Grandja + */ +public class OAuth2ClientCredentialsGrantRequestTests { + private ClientRegistration clientRegistration; + + @Before + public void setup() { + this.clientRegistration = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope("read", "write") + .tokenUri("https://provider.com/oauth2/token") + .build(); + } + + @Test + public void constructorWhenClientRegistrationIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2ClientCredentialsGrantRequest(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenClientRegistrationInvalidGrantTypeThenThrowIllegalArgumentException() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .authorizationGrantType(AuthorizationGrantType.IMPLICIT) + .redirectUriTemplate("https://localhost:8080/redirect-uri") + .authorizationUri("https://provider.com/oauth2/auth") + .clientName("Client 1") + .build(); + + assertThatThrownBy(() -> new OAuth2ClientCredentialsGrantRequest(clientRegistration)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("clientRegistration.authorizationGrantType must be AuthorizationGrantType.CLIENT_CREDENTIALS"); + } + + @Test + public void constructorWhenValidParametersProvidedThenCreated() { + OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = + new OAuth2ClientCredentialsGrantRequest(this.clientRegistration); + + assertThat(clientCredentialsGrantRequest.getClientRegistration()).isEqualTo(this.clientRegistration); + assertThat(clientCredentialsGrantRequest.getGrantType()).isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java similarity index 92% rename from oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClientTests.java rename to oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java index 6f3c93ad24d..5e408841873 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusReactiveAuthorizationCodeTokenResponseClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/WebClientReactiveAuthorizationCodeTokenResponseClientTests.java @@ -44,10 +44,10 @@ * @author Rob Winch * @since 5.1 */ -public class NimbusReactiveAuthorizationCodeTokenResponseClientTests { +public class WebClientReactiveAuthorizationCodeTokenResponseClientTests { private ClientRegistration.Builder clientRegistration; - private NimbusReactiveAuthorizationCodeTokenResponseClient tokenResponseClient = new NimbusReactiveAuthorizationCodeTokenResponseClient(); + private WebClientReactiveAuthorizationCodeTokenResponseClient tokenResponseClient = new WebClientReactiveAuthorizationCodeTokenResponseClient(); private MockWebServer server; @@ -85,6 +85,7 @@ public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() t " \"token_type\": \"bearer\",\n" + " \"expires_in\": \"3600\",\n" + " \"scope\": \"openid profile\",\n" + + " \"refresh_token\": \"refresh-token-1234\",\n" + " \"custom_parameter_1\": \"custom-value-1\",\n" + " \"custom_parameter_2\": \"custom-value-2\"\n" + "}\n"; @@ -102,6 +103,7 @@ public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() t OAuth2AccessToken.TokenType.BEARER); 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().size()).isEqualTo(2); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_1", "custom-value-1"); assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_2", "custom-value-2"); @@ -185,6 +187,17 @@ public void getTokenResponseWhenErrorResponseThenThrowOAuth2AuthenticationExcept .hasMessageContaining("unauthorized_client"); } + // gh-5594 + @Test + public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthenticationException() throws Exception { + String accessTokenErrorResponse = "{}"; + this.server.enqueue(jsonResponse(accessTokenErrorResponse).setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.value())); + + assertThatThrownBy(() -> this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest()).block()) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("server_error"); + } + @Test public void getTokenResponseWhenSuccessResponseAndNotBearerTokenTypeThenThrowOAuth2AuthenticationException() throws Exception { String accessTokenSuccessResponse = "{\n" + diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java index e963bc818e3..cc86577ce05 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -20,6 +20,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; @@ -54,6 +55,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.containsString; @@ -77,7 +79,6 @@ public class OidcAuthorizationCodeAuthenticationProviderTests { private OAuth2AuthorizationExchange authorizationExchange; private OAuth2AccessTokenResponseClient accessTokenResponseClient; private OAuth2AccessTokenResponse accessTokenResponse; - private OAuth2AccessToken accessToken; private OAuth2UserService userService; private OidcAuthorizationCodeAuthenticationProvider authenticationProvider; @@ -93,8 +94,7 @@ public void setUp() throws Exception { this.authorizationResponse = mock(OAuth2AuthorizationResponse.class); this.authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest, this.authorizationResponse); this.accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class); - this.accessTokenResponse = mock(OAuth2AccessTokenResponse.class); - this.accessToken = mock(OAuth2AccessToken.class); + this.accessTokenResponse = this.accessTokenSuccessResponse(); this.userService = mock(OAuth2UserService.class); this.authenticationProvider = PowerMockito.spy( new OidcAuthorizationCodeAuthenticationProvider(this.accessTokenResponseClient, this.userService)); @@ -108,10 +108,6 @@ public void setUp() throws Exception { when(this.authorizationResponse.getState()).thenReturn("12345"); when(this.authorizationRequest.getRedirectUri()).thenReturn("http://example.com"); when(this.authorizationResponse.getRedirectUri()).thenReturn("http://example.com"); - when(this.accessTokenResponse.getAccessToken()).thenReturn(this.accessToken); - Map additionalParameters = new HashMap<>(); - additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token"); - when(this.accessTokenResponse.getAdditionalParameters()).thenReturn(additionalParameters); when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(this.accessTokenResponse); } @@ -190,7 +186,11 @@ public void authenticateWhenTokenResponseDoesNotContainIdTokenThenThrowOAuth2Aut this.exception.expect(OAuth2AuthenticationException.class); this.exception.expectMessage(containsString("invalid_id_token")); - when(this.accessTokenResponse.getAdditionalParameters()).thenReturn(Collections.emptyMap()); + OAuth2AccessTokenResponse accessTokenResponse = + OAuth2AccessTokenResponse.withResponse(this.accessTokenSuccessResponse()) + .additionalParameters(Collections.emptyMap()) + .build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); this.authenticationProvider.authenticate( new OAuth2LoginAuthenticationToken(this.clientRegistration, this.authorizationExchange)); @@ -364,7 +364,8 @@ public void authenticateWhenLoginSuccessThenReturnAuthentication() throws Except assertThat(authentication.getAuthorities()).isEqualTo(authorities); assertThat(authentication.getClientRegistration()).isEqualTo(this.clientRegistration); assertThat(authentication.getAuthorizationExchange()).isEqualTo(this.authorizationExchange); - assertThat(authentication.getAccessToken()).isEqualTo(this.accessToken); + assertThat(authentication.getAccessToken()).isEqualTo(this.accessTokenResponse.getAccessToken()); + assertThat(authentication.getRefreshToken()).isEqualTo(this.accessTokenResponse.getRefreshToken()); } @Test @@ -395,6 +396,30 @@ public void authenticateWhenAuthoritiesMapperSetThenReturnMappedAuthorities() th assertThat(authentication.getAuthorities()).isEqualTo(mappedAuthorities); } + // gh-5368 + @Test + public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToUserRequest() throws Exception { + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://provider.com"); + claims.put(IdTokenClaimNames.SUB, "subject1"); + claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2")); + claims.put(IdTokenClaimNames.AZP, "client1"); + this.setUpIdToken(claims); + + OidcUser principal = mock(OidcUser.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + when(principal.getAuthorities()).thenAnswer( + (Answer>) invocation -> authorities); + ArgumentCaptor userRequestArgCaptor = ArgumentCaptor.forClass(OidcUserRequest.class); + when(this.userService.loadUser(userRequestArgCaptor.capture())).thenReturn(principal); + + this.authenticationProvider.authenticate(new OAuth2LoginAuthenticationToken( + this.clientRegistration, this.authorizationExchange)); + + assertThat(userRequestArgCaptor.getValue().getAdditionalParameters()).containsAllEntriesOf( + this.accessTokenResponse.getAdditionalParameters()); + } + private void setUpIdToken(Map claims) throws Exception { Instant issuedAt = Instant.now(); Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); @@ -411,4 +436,23 @@ private void setUpIdToken(Map claims, Instant issuedAt, Instant when(jwtDecoder.decode(anyString())).thenReturn(idToken); PowerMockito.doReturn(jwtDecoder).when(this.authenticationProvider, "getJwtDecoder", any(ClientRegistration.class)); } + + private OAuth2AccessTokenResponse accessTokenSuccessResponse() { + Instant expiresAt = Instant.now().plusSeconds(5); + Set scopes = new LinkedHashSet<>(Arrays.asList("openid", "profile", "email")); + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token"); + + return OAuth2AccessTokenResponse + .withToken("access-token-1234") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(expiresAt.getEpochSecond()) + .scopes(scopes) + .refreshToken("refresh-token-1234") + .additionalParameters(additionalParameters) + .build(); + + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java new file mode 100644 index 00000000000..2b9bc3061e3 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java @@ -0,0 +1,271 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class OidcAuthorizationCodeReactiveAuthenticationManagerTests { + @Mock + private ReactiveOAuth2UserService userService; + + @Mock + private ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; + + @Mock + private ReactiveOAuth2AuthorizedClientService authorizedClientService; + + @Mock + private ReactiveJwtDecoder jwtDecoder; + + private ClientRegistration.Builder registration = ClientRegistration.withRegistrationId("github") + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .scope("openid") + .authorizationUri("https://github.com/login/oauth/authorize") + .tokenUri("https://github.com/login/oauth/access_token") + .userInfoUri("https://api.github.com/user") + .userNameAttributeName("id") + .clientName("GitHub") + .clientId("clientId") + .jwkSetUri("https://example.com/oauth2/jwk") + .clientSecret("clientSecret"); + + private OAuth2AuthorizationResponse.Builder authorizationResponseBldr = OAuth2AuthorizationResponse + .success("code") + .state("state"); + + private OidcIdToken idToken = new OidcIdToken("token123", Instant.now(), + Instant.now().plusSeconds(3600), Collections.singletonMap(IdTokenClaimNames.SUB, "sub123")); + + private OidcAuthorizationCodeReactiveAuthenticationManager manager; + + @Before + public void setup() { + this.manager = new OidcAuthorizationCodeReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.authorizedClientService); + when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn( + Mono.empty()); + } + + @Test + public void constructorWhenNullAccessTokenResponseClientThenIllegalArgumentException() { + this.accessTokenResponseClient = null; + assertThatThrownBy(() -> new OidcAuthorizationCodeReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.authorizedClientService)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenNullUserServiceThenIllegalArgumentException() { + this.userService = null; + assertThatThrownBy(() -> new OidcAuthorizationCodeReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.authorizedClientService)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenNullAuthorizedClientServiceThenIllegalArgumentException() { + this.authorizedClientService = null; + assertThatThrownBy(() -> new OidcAuthorizationCodeReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService, + this.authorizedClientService)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void authenticateWhenNoSubscriptionThenDoesNothing() { + // we didn't do anything because it should cause a ClassCastException (as verified below) + TestingAuthenticationToken token = new TestingAuthenticationToken("a", "b"); + + assertThatCode(()-> this.manager.authenticate(token)) + .doesNotThrowAnyException(); + + assertThatThrownBy(() -> this.manager.authenticate(token).block()) + .isInstanceOf(Throwable.class); + } + + @Test + public void authenticationWhenNotOidcThenEmpty() { + this.registration.scope("notopenid"); + assertThat(this.manager.authenticate(loginToken()).block()).isNull(); + } + + @Test + public void authenticationWhenErrorThenOAuth2AuthenticationException() { + this.authorizationResponseBldr = OAuth2AuthorizationResponse + .error("error") + .state("state"); + assertThatThrownBy(() -> this.manager.authenticate(loginToken()).block()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void authenticationWhenStateDoesNotMatchThenOAuth2AuthenticationException() { + this.authorizationResponseBldr.state("notmatch"); + assertThatThrownBy(() -> this.manager.authenticate(loginToken()).block()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void authenticationWhenOAuth2UserNotFoundThenEmpty() { + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.")) + .build(); + + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); + claims.put(IdTokenClaimNames.SUB, "rob"); + claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId")); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); + Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, claims, claims); + + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); + when(this.userService.loadUser(any())).thenReturn(Mono.empty()); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken)); + this.manager.setDecoderFactory(c -> this.jwtDecoder); + assertThat(this.manager.authenticate(loginToken()).block()).isNull(); + } + + @Test + public void authenticationWhenOAuth2UserFoundThenSuccess() { + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, this.idToken.getTokenValue())) + .build(); + + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); + claims.put(IdTokenClaimNames.SUB, "rob"); + claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId")); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); + Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, claims, claims); + + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); + DefaultOidcUser user = new DefaultOidcUser(AuthorityUtils.createAuthorityList("ROLE_USER"), this.idToken); + when(this.userService.loadUser(any())).thenReturn(Mono.just(user)); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken)); + this.manager.setDecoderFactory(c -> this.jwtDecoder); + + OAuth2AuthenticationToken result = (OAuth2AuthenticationToken) this.manager.authenticate(loginToken()).block(); + + assertThat(result.getPrincipal()).isEqualTo(user); + assertThat(result.getAuthorities()).containsOnlyElementsOf(user.getAuthorities()); + assertThat(result.isAuthenticated()).isTrue(); + } + + // gh-5368 + @Test + public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToUserRequest() { + Map additionalParameters = new HashMap<>(); + additionalParameters.put(OidcParameterNames.ID_TOKEN, this.idToken.getTokenValue()); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .additionalParameters(additionalParameters) + .build(); + + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); + claims.put(IdTokenClaimNames.SUB, "rob"); + claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId")); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); + Jwt idToken = new Jwt("id-token", issuedAt, expiresAt, claims, claims); + + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); + DefaultOidcUser user = new DefaultOidcUser(AuthorityUtils.createAuthorityList("ROLE_USER"), this.idToken); + ArgumentCaptor userRequestArgCaptor = ArgumentCaptor.forClass(OidcUserRequest.class); + when(this.userService.loadUser(userRequestArgCaptor.capture())).thenReturn(Mono.just(user)); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken)); + this.manager.setDecoderFactory(c -> this.jwtDecoder); + + this.manager.authenticate(loginToken()).block(); + + assertThat(userRequestArgCaptor.getValue().getAdditionalParameters()) + .containsAllEntriesOf(accessTokenResponse.getAdditionalParameters()); + } + + private OAuth2LoginAuthenticationToken loginToken() { + ClientRegistration clientRegistration = this.registration.build(); + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest + .authorizationCode() + .state("state") + .clientId(clientRegistration.getClientId()) + .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .redirectUri(clientRegistration.getRedirectUriTemplate()) + .scopes(clientRegistration.getScopes()) + .build(); + OAuth2AuthorizationResponse authorizationResponse = this.authorizationResponseBldr + .redirectUri(clientRegistration.getRedirectUriTemplate()) + .build(); + OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(authorizationRequest, + authorizationResponse); + return new OAuth2LoginAuthenticationToken(clientRegistration, authorizationExchange); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidatorTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidatorTests.java new file mode 100644 index 00000000000..08d4545fe70 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcTokenValidatorTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class OidcTokenValidatorTests { + private ClientRegistration.Builder registration = ClientRegistration.withRegistrationId("client-foo-bar") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationUri("https://example.com/oauth2/authorize") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .userInfoUri("https://example.com/users/me") + .clientId("client-id") + .clientName("client-name") + .clientSecret("client-secret") + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("user") + .tokenUri("https://example.com/oauth/access_token"); + + private Map claims = new HashMap<>(); + private Instant issuedAt = Instant.now(); + private Instant expiresAt = Instant.now().plusSeconds(3600); + + @Before + public void setup() { + this.claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); + this.claims.put(IdTokenClaimNames.SUB, "rob"); + this.claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id")); + } + + @Test + public void validateIdTokenWhenValidThenNoException() { + assertThatCode(() -> validateIdToken()) + .doesNotThrowAnyException(); + } + + @Test + public void validateIdTokenWhenIssuerNullThenException() { + this.claims.remove(IdTokenClaimNames.ISS); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenSubNullThenException() { + this.claims.remove(IdTokenClaimNames.SUB); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenAudNullThenException() { + this.claims.remove(IdTokenClaimNames.AUD); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenIssuedAtNullThenException() { + this.issuedAt = null; + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenExpiresAtNullThenException() { + this.expiresAt = null; + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenAudMultipleAndAzpNullThenException() { + this.claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id", "other")); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenAzpNotClientIdThenException() { + this.claims.put(IdTokenClaimNames.AZP, "other"); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenMulitpleAudAzpClientIdThenNoException() { + this.claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id", "other")); + this.claims.put(IdTokenClaimNames.AZP, "client-id"); + assertThatCode(() -> validateIdToken()) + .doesNotThrowAnyException(); + } + + @Test + public void validateIdTokenWhenExpiredThenException() { + this.issuedAt = Instant.now().minus(Duration.ofMinutes(1)); + this.expiresAt = this.issuedAt.plus(Duration.ofSeconds(1)); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void validateIdTokenWhenIssuedAtWayInFutureThenException() { + this.issuedAt = Instant.now().plus(Duration.ofMinutes(5)); + this.expiresAt = this.issuedAt.plus(Duration.ofSeconds(1)); + assertThatCode(() -> validateIdToken()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + private void validateIdToken() { + OidcIdToken token = new OidcIdToken("token123", this.issuedAt, this.expiresAt, this.claims); + OidcTokenValidator.validateIdToken(token, this.registration.build()); + } + +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java new file mode 100644 index 00000000000..6da77ea8a30 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.userinfo; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.StandardClaimNames; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class OidcReactiveOAuth2UserServiceTests { + @Mock + private ReactiveOAuth2UserService oauth2UserService; + + private ClientRegistration.Builder registration = ClientRegistration.withRegistrationId("id") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationUri("https://example.com/oauth2/authorize") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .userInfoUri("https://example.com/users/me") + .clientId("client-id") + .clientName("client-name") + .clientSecret("client-secret") + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("user") + .tokenUri("https://example.com/oauth/access_token"); + + private OidcIdToken idToken = new OidcIdToken("token123", Instant.now(), + Instant.now().plusSeconds(3600), Collections + .singletonMap(IdTokenClaimNames.SUB, "sub123")); + + private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "token", + Instant.now(), + Instant.now().plus(Duration.ofDays(1)), + Collections.singleton("user")); + + private OidcReactiveOAuth2UserService userService = new OidcReactiveOAuth2UserService(); + + @Before + public void setup() { + this.userService.setOauth2UserService(this.oauth2UserService); + } + + @Test + public void loadUserWhenUserInfoUriNullThenUserInfoNotRetrieved() { + this.registration.userInfoUri(null); + + OidcUser user = this.userService.loadUser(userRequest()).block(); + + assertThat(user.getUserInfo()).isNull(); + } + + @Test + public void loadUserWhenOAuth2UserEmptyThenNullUserInfo() { + when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.empty()); + + OidcUser user = this.userService.loadUser(userRequest()).block(); + + assertThat(user.getUserInfo()).isNull(); + } + + @Test + public void loadUserWhenOAuth2UserSubjectNullThenOAuth2AuthenticationException() { + OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), Collections.singletonMap("user", "rob"), "user"); + when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User)); + + assertThatCode(() -> this.userService.loadUser(userRequest()).block()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void loadUserWhenOAuth2UserSubjectNotEqualThenOAuth2AuthenticationException() { + Map attributes = new HashMap<>(); + attributes.put(StandardClaimNames.SUB, "not-equal"); + attributes.put("user", "rob"); + OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), + attributes, "user"); + when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User)); + + assertThatCode(() -> this.userService.loadUser(userRequest()).block()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void loadUserWhenOAuth2UserThenUserInfoNotNull() { + Map attributes = new HashMap<>(); + attributes.put(StandardClaimNames.SUB, "sub123"); + attributes.put("user", "rob"); + OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), + attributes, "user"); + when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User)); + + assertThat(this.userService.loadUser(userRequest()).block().getUserInfo()).isNotNull(); + } + + @Test + public void loadUserWhenOAuth2UserAndUser() { + this.registration.userNameAttributeName("user"); + Map attributes = new HashMap<>(); + attributes.put(StandardClaimNames.SUB, "sub123"); + attributes.put("user", "rob"); + OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), + attributes, "user"); + when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User)); + + assertThat(this.userService.loadUser(userRequest()).block().getName()).isEqualTo("rob"); + } + + private OidcUserRequest userRequest() { + return new OidcUserRequest(this.registration.build(), this.accessToken, this.idToken); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestTests.java index ee43d5b1671..afb5a412955 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -17,57 +17,87 @@ import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link OidcUserRequest}. * * @author Joe Grandja */ -@RunWith(PowerMockRunner.class) -@PrepareForTest(ClientRegistration.class) public class OidcUserRequestTests { private ClientRegistration clientRegistration; private OAuth2AccessToken accessToken; private OidcIdToken idToken; + private Map additionalParameters; @Before public void setUp() { - this.clientRegistration = mock(ClientRegistration.class); - this.accessToken = mock(OAuth2AccessToken.class); - this.idToken = mock(OidcIdToken.class); + this.clientRegistration = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("https://client.com") + .scope(new LinkedHashSet<>(Arrays.asList("openid", "profile"))) + .authorizationUri("https://provider.com/oauth2/authorization") + .tokenUri("https://provider.com/oauth2/token") + .jwkSetUri("https://provider.com/keys") + .clientName("Client 1") + .build(); + this.accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "access-token-1234", Instant.now(), Instant.now().plusSeconds(60), + new LinkedHashSet<>(Arrays.asList("scope1", "scope2"))); + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://provider.com"); + claims.put(IdTokenClaimNames.SUB, "subject1"); + claims.put(IdTokenClaimNames.AZP, "client-1"); + this.idToken = new OidcIdToken("id-token-1234", Instant.now(), + Instant.now().plusSeconds(3600), claims); + this.additionalParameters = new HashMap<>(); + this.additionalParameters.put("param1", "value1"); + this.additionalParameters.put("param2", "value2"); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenClientRegistrationIsNullThenThrowIllegalArgumentException() { - new OidcUserRequest(null, this.accessToken, this.idToken); + assertThatThrownBy(() -> new OidcUserRequest(null, this.accessToken, this.idToken)) + .isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenAccessTokenIsNullThenThrowIllegalArgumentException() { - new OidcUserRequest(this.clientRegistration, null, this.idToken); + assertThatThrownBy(() -> new OidcUserRequest(this.clientRegistration, null, this.idToken)) + .isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenIdTokenIsNullThenThrowIllegalArgumentException() { - new OidcUserRequest(this.clientRegistration, this.accessToken, null); + assertThatThrownBy(() -> new OidcUserRequest(this.clientRegistration, this.accessToken, null)) + .isInstanceOf(IllegalArgumentException.class); } @Test public void constructorWhenAllParametersProvidedAndValidThenCreated() { OidcUserRequest userRequest = new OidcUserRequest( - this.clientRegistration, this.accessToken, this.idToken); + this.clientRegistration, this.accessToken, this.idToken, this.additionalParameters); assertThat(userRequest.getClientRegistration()).isEqualTo(this.clientRegistration); assertThat(userRequest.getAccessToken()).isEqualTo(this.accessToken); assertThat(userRequest.getIdToken()).isEqualTo(this.idToken); + assertThat(userRequest.getAdditionalParameters()).containsAllEntriesOf(this.additionalParameters); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtilsTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtilsTests.java new file mode 100644 index 00000000000..9851c7fdd2a --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtilsTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.userinfo; + +import org.junit.Test; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class OidcUserRequestUtilsTests { + private ClientRegistration.Builder registration = ClientRegistration.withRegistrationId("id") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationUri("https://example.com/oauth2/authorize") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .userInfoUri("https://example.com/users/me") + .clientId("client-id") + .clientName("client-name") + .clientSecret("client-secret") + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("user") + .tokenUri("https://example.com/oauth/access_token"); + + OidcIdToken idToken = new OidcIdToken("token123", Instant.now(), + Instant.now().plusSeconds(3600), Collections + .singletonMap(IdTokenClaimNames.SUB, "sub123")); + + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "token", + Instant.now(), + Instant.now().plus(Duration.ofDays(1)), + Collections.singleton("user")); + + @Test + public void shouldRetrieveUserInfoWhenEndpointDefinedAndScopesOverlapThenTrue() { + assertThat(OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest())).isTrue(); + } + + @Test + public void shouldRetrieveUserInfoWhenNoUserInfoUriThenFalse() { + this.registration.userInfoUri(null); + + assertThat(OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest())).isFalse(); + } + + @Test + public void shouldRetrieveUserInfoWhenDifferentScopesThenFalse() { + this.registration.scope("notintoken"); + + assertThat(OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest())).isFalse(); + } + + @Test + public void shouldRetrieveUserInfoWhenNotAuthorizationCodeThenFalse() { + this.registration.authorizationGrantType(AuthorizationGrantType.IMPLICIT); + + assertThat(OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest())).isFalse(); + } + + private OidcUserRequest userRequest() { + return new OidcUserRequest(this.registration.build(), this.accessToken, this.idToken); + } +} 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 ba08c8163b0..e388a800fdf 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 @@ -17,6 +17,7 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -26,9 +27,12 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -47,6 +51,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -56,7 +61,7 @@ * * @author Joe Grandja */ -@PowerMockIgnore("okhttp3.*") +@PowerMockIgnore({"okhttp3.*", "okio.Buffer"}) @PrepareForTest(ClientRegistration.class) @RunWith(PowerMockRunner.class) public class OidcUserServiceTests { @@ -79,6 +84,9 @@ public void setUp() throws Exception { when(this.providerDetails.getUserInfoEndpoint()).thenReturn(this.userInfoEndpoint); when(this.clientRegistration.getAuthorizationGrantType()).thenReturn(AuthorizationGrantType.AUTHORIZATION_CODE); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); + when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn(StandardClaimNames.SUB); + this.accessToken = mock(OAuth2AccessToken.class); Set authorizedScopes = new LinkedHashSet<>(Arrays.asList(OidcScopes.OPENID, OidcScopes.PROFILE)); when(this.accessToken.getScopes()).thenReturn(authorizedScopes); @@ -89,6 +97,14 @@ public void setUp() throws Exception { idTokenClaims.put(IdTokenClaimNames.SUB, "subject1"); when(this.idToken.getClaims()).thenReturn(idTokenClaims); when(this.idToken.getSubject()).thenReturn("subject1"); + + this.userService.setOauth2UserService(new DefaultOAuth2UserService()); + } + + @Test + public void setOauth2UserServiceWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.userService.setOauth2UserService(null)) + .isInstanceOf(IllegalArgumentException.class); } @Test @@ -166,6 +182,37 @@ public void loadUserWhenUserInfoSuccessResponseThenReturnUser() throws Exception assertThat(userAuthority.getUserInfo()).isEqualTo(user.getUserInfo()); } + // gh-5447 + @Test + public void loadUserWhenUserInfoSuccessResponseAndUserInfoSubjectIsNullThenThrowOAuth2AuthenticationException() throws Exception { + this.exception.expect(OAuth2AuthenticationException.class); + this.exception.expectMessage(containsString("invalid_user_info_response")); + + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"email\": \"full_name@provider.com\",\n" + + " \"name\": \"full name\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn(StandardClaimNames.EMAIL); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + try { + this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken)); + } finally { + server.shutdown(); + } + } + @Test public void loadUserWhenUserInfoSuccessResponseAndUserInfoSubjectNotSameAsIdTokenSubjectThenThrowOAuth2AuthenticationException() throws Exception { this.exception.expect(OAuth2AuthenticationException.class); @@ -321,4 +368,71 @@ public void loadUserWhenUserInfoSuccessResponseThenAcceptHeaderJson() throws Exc assertThat(server.takeRequest(1, TimeUnit.SECONDS).getHeader(HttpHeaders.ACCEPT)) .isEqualTo(MediaType.APPLICATION_JSON_VALUE); } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodGet() throws Exception { + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"sub\": \"subject1\",\n" + + " \"name\": \"first last\",\n" + + " \"given_name\": \"first\",\n" + + " \"family_name\": \"last\",\n" + + " \"preferred_username\": \"user1\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken)); + server.shutdown(); + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPost() throws Exception { + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"sub\": \"subject1\",\n" + + " \"name\": \"first last\",\n" + + " \"given_name\": \"first\",\n" + + " \"family_name\": \"last\",\n" + + " \"preferred_username\": \"user1\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.FORM); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken)); + server.shutdown(); + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE); + assertThat(request.getBody().readUtf8()).isEqualTo("access_token=" + this.accessToken.getTokenValue()); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java index a9b1d7a7149..438894fab5c 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.client.registration; import org.junit.Test; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; @@ -24,6 +25,7 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link ClientRegistration}. @@ -52,6 +54,7 @@ public void buildWhenAuthorizationGrantTypeIsNullThenThrowIllegalArgumentExcepti .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -68,6 +71,7 @@ public void buildWhenAuthorizationCodeGrantAllAttributesProvidedThenAllAttribute .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -81,6 +85,7 @@ public void buildWhenAuthorizationCodeGrantAllAttributesProvidedThenAllAttribute assertThat(registration.getScopes()).isEqualTo(SCOPES); assertThat(registration.getProviderDetails().getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI); assertThat(registration.getProviderDetails().getTokenUri()).isEqualTo(TOKEN_URI); + assertThat(registration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()).isEqualTo(AuthenticationMethod.FORM); assertThat(registration.getProviderDetails().getJwkSetUri()).isEqualTo(JWK_SET_URI); assertThat(registration.getClientName()).isEqualTo(CLIENT_NAME); } @@ -96,6 +101,7 @@ public void buildWhenAuthorizationCodeGrantRegistrationIdIsNullThenThrowIllegalA .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -112,41 +118,45 @@ public void buildWhenAuthorizationCodeGrantClientIdIsNullThenThrowIllegalArgumen .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); } - @Test(expected = IllegalArgumentException.class) - public void buildWhenAuthorizationCodeGrantClientSecretIsNullThenThrowIllegalArgumentException() { - ClientRegistration.withRegistrationId(REGISTRATION_ID) - .clientId(CLIENT_ID) - .clientSecret(null) - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate(REDIRECT_URI) - .scope(SCOPES.toArray(new String[0])) - .authorizationUri(AUTHORIZATION_URI) - .tokenUri(TOKEN_URI) - .jwkSetUri(JWK_SET_URI) - .clientName(CLIENT_NAME) - .build(); + @Test + public void buildWhenAuthorizationCodeGrantClientSecretIsNullThenDefaultToEmpty() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(null) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(REDIRECT_URI) + .scope(SCOPES.toArray(new String[0])) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) + .jwkSetUri(JWK_SET_URI) + .clientName(CLIENT_NAME) + .build(); + assertThat(clientRegistration.getClientSecret()).isEqualTo(""); } - @Test(expected = IllegalArgumentException.class) - public void buildWhenAuthorizationCodeGrantClientAuthenticationMethodIsNullThenThrowIllegalArgumentException() { - ClientRegistration.withRegistrationId(REGISTRATION_ID) - .clientId(CLIENT_ID) - .clientSecret(CLIENT_SECRET) - .clientAuthenticationMethod(null) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate(REDIRECT_URI) - .scope(SCOPES.toArray(new String[0])) - .authorizationUri(AUTHORIZATION_URI) - .tokenUri(TOKEN_URI) - .jwkSetUri(JWK_SET_URI) - .clientName(CLIENT_NAME) - .build(); + @Test + public void buildWhenAuthorizationCodeGrantClientAuthenticationMethodNotProvidedThenDefaultToBasic() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(REDIRECT_URI) + .scope(SCOPES.toArray(new String[0])) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) + .jwkSetUri(JWK_SET_URI) + .clientName(CLIENT_NAME) + .build(); + assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); } @Test(expected = IllegalArgumentException.class) @@ -160,13 +170,15 @@ public void buildWhenAuthorizationCodeGrantRedirectUriIsNullThenThrowIllegalArgu .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); } - @Test(expected = IllegalArgumentException.class) - public void buildWhenAuthorizationCodeGrantScopeIsNullThenThrowIllegalArgumentException() { + // gh-5494 + @Test + public void buildWhenAuthorizationCodeGrantScopeIsNullThenScopeNotRequired() { ClientRegistration.withRegistrationId(REGISTRATION_ID) .clientId(CLIENT_ID) .clientSecret(CLIENT_SECRET) @@ -176,6 +188,7 @@ public void buildWhenAuthorizationCodeGrantScopeIsNullThenThrowIllegalArgumentEx .scope((String[]) null) .authorizationUri(AUTHORIZATION_URI) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -192,6 +205,7 @@ public void buildWhenAuthorizationCodeGrantAuthorizationUriIsNullThenThrowIllega .scope(SCOPES.toArray(new String[0])) .authorizationUri(null) .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); @@ -208,41 +222,27 @@ public void buildWhenAuthorizationCodeGrantTokenUriIsNullThenThrowIllegalArgumen .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) .tokenUri(null) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) .clientName(CLIENT_NAME) .build(); } - @Test(expected = IllegalArgumentException.class) - public void buildWhenAuthorizationCodeGrantJwkSetUriIsNullThenThrowIllegalArgumentException() { - ClientRegistration.withRegistrationId(REGISTRATION_ID) - .clientId(CLIENT_ID) - .clientSecret(CLIENT_SECRET) - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate(REDIRECT_URI) - .scope(SCOPES.toArray(new String[0])) - .authorizationUri(AUTHORIZATION_URI) - .tokenUri(TOKEN_URI) - .jwkSetUri(null) - .clientName(CLIENT_NAME) - .build(); - } - - @Test(expected = IllegalArgumentException.class) - public void buildWhenAuthorizationCodeGrantClientNameIsNullThenThrowIllegalArgumentException() { - ClientRegistration.withRegistrationId(REGISTRATION_ID) - .clientId(CLIENT_ID) - .clientSecret(CLIENT_SECRET) - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate(REDIRECT_URI) - .scope(SCOPES.toArray(new String[0])) - .authorizationUri(AUTHORIZATION_URI) - .tokenUri(TOKEN_URI) - .jwkSetUri(JWK_SET_URI) - .clientName(null) - .build(); + @Test + public void buildWhenAuthorizationCodeGrantClientNameNotProvidedThenDefaultToRegistrationId() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(REDIRECT_URI) + .scope(SCOPES.toArray(new String[0])) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) + .jwkSetUri(JWK_SET_URI) + .build(); + assertThat(clientRegistration.getClientName()).isEqualTo(clientRegistration.getRegistrationId()); } @Test @@ -255,11 +255,27 @@ public void buildWhenAuthorizationCodeGrantScopeDoesNotContainOpenidThenJwkSetUr .redirectUriTemplate(REDIRECT_URI) .scope("scope1") .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .tokenUri(TOKEN_URI) .clientName(CLIENT_NAME) .build(); } + // gh-5494 + @Test + public void buildWhenAuthorizationCodeGrantScopeIsNullThenJwkSetUriNotRequired() { + ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(REDIRECT_URI) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI) + .clientName(CLIENT_NAME) + .build(); + } + @Test public void buildWhenImplicitGrantAllAttributesProvidedThenAllAttributesAreSet() { ClientRegistration registration = ClientRegistration.withRegistrationId(REGISTRATION_ID) @@ -268,6 +284,7 @@ public void buildWhenImplicitGrantAllAttributesProvidedThenAllAttributesAreSet() .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); @@ -277,6 +294,7 @@ public void buildWhenImplicitGrantAllAttributesProvidedThenAllAttributesAreSet() assertThat(registration.getRedirectUriTemplate()).isEqualTo(REDIRECT_URI); assertThat(registration.getScopes()).isEqualTo(SCOPES); assertThat(registration.getProviderDetails().getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI); + assertThat(registration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()).isEqualTo(AuthenticationMethod.FORM); assertThat(registration.getClientName()).isEqualTo(CLIENT_NAME); } @@ -288,6 +306,7 @@ public void buildWhenImplicitGrantRegistrationIdIsNullThenThrowIllegalArgumentEx .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -300,6 +319,7 @@ public void buildWhenImplicitGrantClientIdIsNullThenThrowIllegalArgumentExceptio .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -312,18 +332,21 @@ public void buildWhenImplicitGrantRedirectUriIsNullThenThrowIllegalArgumentExcep .redirectUriTemplate(null) .scope(SCOPES.toArray(new String[0])) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } - @Test(expected = IllegalArgumentException.class) - public void buildWhenImplicitGrantScopeIsNullThenThrowIllegalArgumentException() { + // gh-5494 + @Test + public void buildWhenImplicitGrantScopeIsNullThenScopeNotRequired() { ClientRegistration.withRegistrationId(REGISTRATION_ID) .clientId(CLIENT_ID) .authorizationGrantType(AuthorizationGrantType.IMPLICIT) .redirectUriTemplate(REDIRECT_URI) .scope((String[]) null) .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } @@ -336,19 +359,125 @@ public void buildWhenImplicitGrantAuthorizationUriIsNullThenThrowIllegalArgument .redirectUriTemplate(REDIRECT_URI) .scope(SCOPES.toArray(new String[0])) .authorizationUri(null) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .clientName(CLIENT_NAME) .build(); } - @Test(expected = IllegalArgumentException.class) - public void buildWhenImplicitGrantClientNameIsNullThenThrowIllegalArgumentException() { - ClientRegistration.withRegistrationId(REGISTRATION_ID) - .clientId(CLIENT_ID) - .authorizationGrantType(AuthorizationGrantType.IMPLICIT) - .redirectUriTemplate(REDIRECT_URI) - .scope(SCOPES.toArray(new String[0])) - .authorizationUri(AUTHORIZATION_URI) - .clientName(null) - .build(); + @Test + public void buildWhenImplicitGrantClientNameNotProvidedThenDefaultToRegistrationId() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .authorizationGrantType(AuthorizationGrantType.IMPLICIT) + .redirectUriTemplate(REDIRECT_URI) + .scope(SCOPES.toArray(new String[0])) + .authorizationUri(AUTHORIZATION_URI) + .userInfoAuthenticationMethod(AuthenticationMethod.FORM) + .build(); + assertThat(clientRegistration.getClientName()).isEqualTo(clientRegistration.getRegistrationId()); + } + + @Test + public void buildWhenOverrideRegistrationIdThenOverridden() { + String overriddenId = "override"; + ClientRegistration registration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .registrationId(overriddenId) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(REDIRECT_URI) + .scope(SCOPES.toArray(new String[0])) + .authorizationUri(AUTHORIZATION_URI) + .tokenUri(TOKEN_URI) + .jwkSetUri(JWK_SET_URI) + .clientName(CLIENT_NAME) + .build(); + + assertThat(registration.getRegistrationId()).isEqualTo(overriddenId); + } + + @Test + public void buildWhenClientCredentialsGrantAllAttributesProvidedThenAllAttributesAreSet() { + ClientRegistration registration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope(SCOPES.toArray(new String[0])) + .tokenUri(TOKEN_URI) + .clientName(CLIENT_NAME) + .build(); + + assertThat(registration.getRegistrationId()).isEqualTo(REGISTRATION_ID); + assertThat(registration.getClientId()).isEqualTo(CLIENT_ID); + assertThat(registration.getClientSecret()).isEqualTo(CLIENT_SECRET); + assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.CLIENT_CREDENTIALS); + assertThat(registration.getScopes()).isEqualTo(SCOPES); + assertThat(registration.getProviderDetails().getTokenUri()).isEqualTo(TOKEN_URI); + assertThat(registration.getClientName()).isEqualTo(CLIENT_NAME); + } + + @Test + public void buildWhenClientCredentialsGrantRegistrationIdIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> + ClientRegistration.withRegistrationId(null) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri(TOKEN_URI) + .build() + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void buildWhenClientCredentialsGrantClientIdIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> + ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(null) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri(TOKEN_URI) + .build() + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void buildWhenClientCredentialsGrantClientSecretIsNullThenDefaultToEmpty() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(null) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri(TOKEN_URI) + .build(); + assertThat(clientRegistration.getClientSecret()).isEqualTo(""); + } + + @Test + public void buildWhenClientCredentialsGrantClientAuthenticationMethodNotProvidedThenDefaultToBasic() { + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri(TOKEN_URI) + .build(); + assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + } + + @Test + public void buildWhenClientCredentialsGrantTokenUriIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> + ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri(null) + .build() + ).isInstanceOf(IllegalArgumentException.class); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java new file mode 100644 index 00000000000..eb2e29fb1fa --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java @@ -0,0 +1,241 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.Arrays; +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class ClientRegistrationsTest { + + /** + * Contains all optional parameters that are found in ClientRegistration + */ + private static final String DEFAULT_RESPONSE = + "{\n" + + " \"authorization_endpoint\": \"https://example.com/o/oauth2/v2/auth\", \n" + + " \"claims_supported\": [\n" + + " \"aud\", \n" + + " \"email\", \n" + + " \"email_verified\", \n" + + " \"exp\", \n" + + " \"family_name\", \n" + + " \"given_name\", \n" + + " \"iat\", \n" + + " \"iss\", \n" + + " \"locale\", \n" + + " \"name\", \n" + + " \"picture\", \n" + + " \"sub\"\n" + + " ], \n" + + " \"code_challenge_methods_supported\": [\n" + + " \"plain\", \n" + + " \"S256\"\n" + + " ], \n" + + " \"id_token_signing_alg_values_supported\": [\n" + + " \"RS256\"\n" + + " ], \n" + + " \"issuer\": \"https://example.com\", \n" + + " \"jwks_uri\": \"https://example.com/oauth2/v3/certs\", \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" + + " \"revocation_endpoint\": \"https://example.com/o/oauth2/revoke\", \n" + + " \"scopes_supported\": [\n" + + " \"openid\", \n" + + " \"email\", \n" + + " \"profile\"\n" + + " ], \n" + + " \"subject_types_supported\": [\n" + + " \"public\"\n" + + " ], \n" + + " \"grant_types_supported\" : [\"authorization_code\"], \n" + + " \"token_endpoint\": \"https://example.com/oauth2/v4/token\", \n" + + " \"token_endpoint_auth_methods_supported\": [\n" + + " \"client_secret_post\", \n" + + " \"client_secret_basic\"\n" + + " ], \n" + + " \"userinfo_endpoint\": \"https://example.com/oauth2/v3/userinfo\"\n" + + "}"; + + private MockWebServer server; + + private ObjectMapper mapper = new ObjectMapper(); + + private Map response; + + private String issuer; + + @Before + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.response = this.mapper.readValue(DEFAULT_RESPONSE, new TypeReference>(){}); + } + + @After + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void issuerWhenAllInformationThenSuccess() throws Exception { + ClientRegistration registration = registration(""); + ClientRegistration.ProviderDetails provider = registration.getProviderDetails(); + + assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(registration.getRegistrationId()).isEqualTo(this.server.getHostName()); + assertThat(registration.getClientName()).isEqualTo(this.issuer); + assertThat(registration.getScopes()).containsOnly("openid", "email", "profile"); + assertThat(provider.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth"); + assertThat(provider.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token"); + assertThat(provider.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs"); + assertThat(provider.getUserInfoEndpoint().getUri()).isEqualTo("https://example.com/oauth2/v3/userinfo"); + } + + /** + * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + * + * RECOMMENDED. JSON array containing a list of the OAuth 2.0 [RFC6749] scope values that this server supports. The + * server MUST support the openid scope value. + * @throws Exception + */ + @Test + public void issuerWhenScopesNullThenScopesDefaulted() throws Exception { + this.response.remove("scopes_supported"); + + ClientRegistration registration = registration(""); + + assertThat(registration.getScopes()).containsOnly("openid"); + } + + @Test + public void issuerWhenGrantTypesSupportedNullThenDefaulted() throws Exception { + this.response.remove("grant_types_supported"); + + ClientRegistration registration = registration(""); + + assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + } + + /** + * We currently only support authorization_code, so verify we have a meaningful error until we add support. + * @throws Exception + */ + public void issuerWhenGrantTypesSupportedInvalidThenException() throws Exception { + this.response.put("grant_types_supported", Arrays.asList("implicit")); + + assertThatThrownBy(() -> registration("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Only AuthorizationGrantType.AUTHORIZATION_CODE is supported. The issuer \"" + this.issuer + "\" returned a configuration of [implicit]"); + } + + @Test + public void issuerWhenTokenEndpointAuthMethodsNullThenDefaulted() throws Exception { + this.response.remove("token_endpoint_auth_methods_supported"); + + ClientRegistration registration = registration(""); + + assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + } + + @Test + public void issuerWhenTokenEndpointAuthMethodsPostThenMethodIsPost() throws Exception { + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("client_secret_post")); + + ClientRegistration registration = registration(""); + + assertThat(registration.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.POST); + } + + /** + * We currently only support client_secret_basic, so verify we have a meaningful error until we add support. + * @throws Exception + */ + @Test + public void issuerWhenTokenEndpointAuthMethodsInvalidThenException() throws Exception { + this.response.put("token_endpoint_auth_methods_supported", Arrays.asList("tls_client_auth")); + + assertThatThrownBy(() -> registration("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Only ClientAuthenticationMethod.BASIC and ClientAuthenticationMethod.POST are supported. The issuer \"" + this.issuer + "\" returned a configuration of [tls_client_auth]"); + } + + @Test + public void issuerWhenEmptyStringThenMeaningfulErrorMessage() { + assertThatThrownBy(() -> ClientRegistrations.fromOidcIssuerLocation("")) + .hasMessageContaining("Unable to resolve the OpenID Configuration with the provided Issuer of \"\""); + } + + @Test + public void issuerWhenOpenIdConfigurationDoesNotMatchThenMeaningfulErrorMessage() throws Exception { + this.issuer = createIssuerFromServer(""); + String body = this.mapper.writeValueAsString(this.response); + MockResponse mockResponse = new MockResponse() + .setBody(body) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + this.server.enqueue(mockResponse); + assertThatThrownBy(() -> ClientRegistrations.fromOidcIssuerLocation(this.issuer)) + .hasMessageContaining("The Issuer \"https://example.com\" provided in the OpenID Configuration did not match the requested issuer \"" + this.issuer + "\""); + } + + private ClientRegistration registration(String path) throws Exception { + this.issuer = createIssuerFromServer(path); + this.response.put("issuer", this.issuer); + String body = this.mapper.writeValueAsString(this.response); + MockResponse mockResponse = new MockResponse() + .setBody(body) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + this.server.enqueue(mockResponse); + + return ClientRegistrations.fromOidcIssuerLocation(this.issuer) + .clientId("client-id") + .clientSecret("client-secret") + .build(); + } + + private String createIssuerFromServer(String path) { + return this.server.url(path).toString(); + } +} 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 a85a99f2155..99ca960c2ee 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 @@ -17,6 +17,8 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -26,9 +28,11 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; @@ -46,7 +50,7 @@ * * @author Joe Grandja */ -@PowerMockIgnore("okhttp3.*") +@PowerMockIgnore({"okhttp3.*", "okio.Buffer"}) @PrepareForTest(ClientRegistration.class) @RunWith(PowerMockRunner.class) public class DefaultOAuth2UserServiceTests { @@ -115,6 +119,7 @@ public void loadUserWhenUserInfoSuccessResponseThenReturnUser() throws Exception String userInfoUri = server.url("/user").toString(); when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -162,6 +167,7 @@ public void loadUserWhenUserInfoSuccessResponseInvalidThenThrowOAuth2Authenticat String userInfoUri = server.url("/user").toString(); when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -184,6 +190,7 @@ public void loadUserWhenUserInfoErrorResponseThenThrowOAuth2AuthenticationExcept String userInfoUri = server.url("/user").toString(); when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -201,6 +208,7 @@ public void loadUserWhenUserInfoUriInvalidThenThrowAuthenticationServiceExceptio String userInfoUri = "http://invalid-provider.com/user"; when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -229,6 +237,7 @@ public void loadUserWhenUserInfoSuccessResponseThenAcceptHeaderJson() throws Exc String userInfoUri = server.url("/user").toString(); when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); when(this.accessToken.getTokenValue()).thenReturn("access-token"); @@ -237,4 +246,73 @@ public void loadUserWhenUserInfoSuccessResponseThenAcceptHeaderJson() throws Exc assertThat(server.takeRequest(1, TimeUnit.SECONDS).getHeader(HttpHeaders.ACCEPT)) .isEqualTo(MediaType.APPLICATION_JSON_VALUE); } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodGet() throws Exception { + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"user-name\": \"user1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.HEADER); + when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + this.userService.loadUser(new OAuth2UserRequest(this.clientRegistration, this.accessToken)); + server.shutdown(); + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPost() throws Exception { + MockWebServer server = new MockWebServer(); + + String userInfoResponse = "{\n" + + " \"user-name\": \"user1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + server.enqueue(new MockResponse() + .setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .setBody(userInfoResponse)); + + server.start(); + + String userInfoUri = server.url("/user").toString(); + + when(this.userInfoEndpoint.getUri()).thenReturn(userInfoUri); + when(this.userInfoEndpoint.getAuthenticationMethod()).thenReturn(AuthenticationMethod.FORM); + when(this.userInfoEndpoint.getUserNameAttributeName()).thenReturn("user-name"); + when(this.accessToken.getTokenValue()).thenReturn("access-token"); + + this.userService.loadUser(new OAuth2UserRequest(this.clientRegistration, this.accessToken)); + server.shutdown(); + RecordedRequest request = server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE); + assertThat(request.getBody().readUtf8()).isEqualTo("access_token=" + this.accessToken.getTokenValue()); + } } 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 70090f3d896..be57a92114a 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 @@ -22,15 +22,19 @@ import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; + +import okhttp3.mockwebserver.RecordedRequest; import reactor.test.StepVerifier; import java.time.Duration; @@ -67,6 +71,7 @@ public void setup() throws Exception { .authorizationUri("https://github.com/login/oauth/authorize") .tokenUri("https://github.com/login/oauth/access_token") .userInfoUri(userInfoUri) + .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) .userNameAttributeName("user-name") .clientName("GitHub") .clientId("clientId") @@ -140,6 +145,51 @@ public void loadUserWhenUserInfoSuccessResponseThenReturnUser() throws Exception assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes()); } + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodGet() throws Exception { + this.clientRegistration.userInfoAuthenticationMethod(AuthenticationMethod.HEADER); + String userInfoResponse = "{\n" + + " \"user-name\": \"user1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + enqueueApplicationJsonBody(userInfoResponse); + + this.userService.loadUser(oauth2UserRequest()).block(); + + RecordedRequest request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + // gh-5500 + @Test + public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPost() throws Exception { + this.clientRegistration.userInfoAuthenticationMethod( AuthenticationMethod.FORM); + String userInfoResponse = "{\n" + + " \"user-name\": \"user1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + enqueueApplicationJsonBody(userInfoResponse); + + this.userService.loadUser(oauth2UserRequest()).block(); + + RecordedRequest request = this.server.takeRequest(); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name()); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE); + assertThat(request.getBody().readUtf8()).isEqualTo("access_token=" + this.accessToken.getTokenValue()); + } + @Test public void loadUserWhenUserInfoSuccessResponseInvalidThenThrowOAuth2AuthenticationException() throws Exception { String userInfoResponse = "{\n" + diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestTests.java index 59a054aaa76..6415721604d 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -17,47 +17,70 @@ import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link OAuth2UserRequest}. * * @author Joe Grandja */ -@RunWith(PowerMockRunner.class) -@PrepareForTest(ClientRegistration.class) public class OAuth2UserRequestTests { private ClientRegistration clientRegistration; private OAuth2AccessToken accessToken; + private Map additionalParameters; @Before public void setUp() { - this.clientRegistration = mock(ClientRegistration.class); - this.accessToken = mock(OAuth2AccessToken.class); + this.clientRegistration = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("https://client.com") + .scope(new LinkedHashSet<>(Arrays.asList("scope1", "scope2"))) + .authorizationUri("https://provider.com/oauth2/authorization") + .tokenUri("https://provider.com/oauth2/token") + .clientName("Client 1") + .build(); + this.accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "access-token-1234", Instant.now(), Instant.now().plusSeconds(60), + new LinkedHashSet<>(Arrays.asList("scope1", "scope2"))); + this.additionalParameters = new HashMap<>(); + this.additionalParameters.put("param1", "value1"); + this.additionalParameters.put("param2", "value2"); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenClientRegistrationIsNullThenThrowIllegalArgumentException() { - new OAuth2UserRequest(null, this.accessToken); + assertThatThrownBy(() -> new OAuth2UserRequest(null, this.accessToken)) + .isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenAccessTokenIsNullThenThrowIllegalArgumentException() { - new OAuth2UserRequest(this.clientRegistration, null); + assertThatThrownBy(() -> new OAuth2UserRequest(this.clientRegistration, null)) + .isInstanceOf(IllegalArgumentException.class); } @Test public void constructorWhenAllParametersProvidedAndValidThenCreated() { - OAuth2UserRequest userRequest = new OAuth2UserRequest(this.clientRegistration, this.accessToken); + OAuth2UserRequest userRequest = new OAuth2UserRequest( + this.clientRegistration, this.accessToken, this.additionalParameters); assertThat(userRequest.getClientRegistration()).isEqualTo(this.clientRegistration); assertThat(userRequest.getAccessToken()).isEqualTo(this.accessToken); + assertThat(userRequest.getAdditionalParameters()).containsAllEntriesOf(this.additionalParameters); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepositoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepositoryTests.java new file mode 100644 index 00000000000..e24c1e74d97 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepositoryTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AuthenticatedPrincipalOAuth2AuthorizedClientRepository}. + * + * @author Joe Grandja + */ +public class AuthenticatedPrincipalOAuth2AuthorizedClientRepositoryTests { + private String registrationId = "registrationId"; + private String principalName = "principalName"; + private OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository anonymousAuthorizedClientRepository; + private AuthenticatedPrincipalOAuth2AuthorizedClientRepository authorizedClientRepository; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + @Before + public void setup() { + this.authorizedClientService = mock(OAuth2AuthorizedClientService.class); + this.anonymousAuthorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class); + this.authorizedClientRepository = new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(this.authorizedClientService); + this.authorizedClientRepository.setAnonymousAuthorizedClientRepository(this.anonymousAuthorizedClientRepository); + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + } + + @Test + public void constructorWhenAuthorizedClientServiceIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void setAuthorizedClientRepositoryWhenAuthorizedClientRepositoryIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizedClientRepository.setAnonymousAuthorizedClientRepository(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void loadAuthorizedClientWhenAuthenticatedPrincipalThenLoadFromService() { + Authentication authentication = this.createAuthenticatedPrincipal(); + this.authorizedClientRepository.loadAuthorizedClient(this.registrationId, authentication, this.request); + verify(this.authorizedClientService).loadAuthorizedClient(this.registrationId, this.principalName); + } + + @Test + public void loadAuthorizedClientWhenAnonymousPrincipalThenLoadFromAnonymousRepository() { + Authentication authentication = this.createAnonymousPrincipal(); + this.authorizedClientRepository.loadAuthorizedClient(this.registrationId, authentication, this.request); + verify(this.anonymousAuthorizedClientRepository).loadAuthorizedClient(this.registrationId, authentication, this.request); + } + + @Test + public void saveAuthorizedClientWhenAuthenticatedPrincipalThenSaveToService() { + Authentication authentication = this.createAuthenticatedPrincipal(); + OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, authentication, this.request, this.response); + verify(this.authorizedClientService).saveAuthorizedClient(authorizedClient, authentication); + } + + @Test + public void saveAuthorizedClientWhenAnonymousPrincipalThenSaveToAnonymousRepository() { + Authentication authentication = this.createAnonymousPrincipal(); + OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, authentication, this.request, this.response); + verify(this.anonymousAuthorizedClientRepository).saveAuthorizedClient(authorizedClient, authentication, this.request, this.response); + } + + @Test + public void removeAuthorizedClientWhenAuthenticatedPrincipalThenRemoveFromService() { + Authentication authentication = this.createAuthenticatedPrincipal(); + this.authorizedClientRepository.removeAuthorizedClient(this.registrationId, authentication, this.request, this.response); + verify(this.authorizedClientService).removeAuthorizedClient(this.registrationId, this.principalName); + } + + @Test + public void removeAuthorizedClientWhenAnonymousPrincipalThenRemoveFromAnonymousRepository() { + Authentication authentication = this.createAnonymousPrincipal(); + this.authorizedClientRepository.removeAuthorizedClient(this.registrationId, authentication, this.request, this.response); + verify(this.anonymousAuthorizedClientRepository).removeAuthorizedClient(this.registrationId, authentication, this.request, this.response); + } + + private Authentication createAuthenticatedPrincipal() { + TestingAuthenticationToken authentication = new TestingAuthenticationToken(this.principalName, "password"); + authentication.setAuthenticated(true); + return authentication; + } + + private Authentication createAnonymousPrincipal() { + return new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java new file mode 100644 index 00000000000..b5d548c8477 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java @@ -0,0 +1,258 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; +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.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for {@link DefaultOAuth2AuthorizationRequestResolver}. + * + * @author Joe Grandja + */ +public class DefaultOAuth2AuthorizationRequestResolverTests { + private ClientRegistration registration1; + private ClientRegistration registration2; + private ClientRegistrationRepository clientRegistrationRepository; + private String authorizationRequestBaseUri = "/oauth2/authorization"; + private DefaultOAuth2AuthorizationRequestResolver resolver; + + @Before + public void setUp() { + this.registration1 = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .scope("user") + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .userInfoUri("https://provider.com/oauth2/user") + .userNameAttributeName("id") + .clientName("client-1") + .build(); + this.registration2 = ClientRegistration.withRegistrationId("registration-2") + .clientId("client-2") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .scope("openid", "profile", "email") + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .userInfoUri("https://provider.com/oauth2/userinfo") + .jwkSetUri("https://provider.com/oauth2/keys") + .clientName("client-2") + .build(); + this.clientRegistrationRepository = new InMemoryClientRegistrationRepository( + this.registration1, this.registration2); + this.resolver = new DefaultOAuth2AuthorizationRequestResolver( + this.clientRegistrationRepository, this.authorizationRequestBaseUri); + } + + @Test + public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new DefaultOAuth2AuthorizationRequestResolver(null, this.authorizationRequestBaseUri)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenAuthorizationRequestBaseUriIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new DefaultOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository, null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void resolveWhenNotAuthorizationRequestThenDoesNotResolve() { + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest).isNull(); + } + + @Test + public void resolveWhenAuthorizationRequestWithInvalidClientThenThrowIllegalArgumentException() { + ClientRegistration clientRegistration = this.registration1; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId() + "-invalid"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + + assertThatThrownBy(() -> this.resolver.resolve(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid Client Registration with Id: " + clientRegistration.getRegistrationId() + "-invalid"); + } + + @Test + public void resolveWhenAuthorizationRequestWithValidClientThenResolves() { + ClientRegistration clientRegistration = this.registration1; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest).isNotNull(); + assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo( + clientRegistration.getProviderDetails().getAuthorizationUri()); + assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE); + assertThat(authorizationRequest.getClientId()).isEqualTo(clientRegistration.getClientId()); + assertThat(authorizationRequest.getRedirectUri()) + .isEqualTo("http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId()); + assertThat(authorizationRequest.getScopes()).isEqualTo(clientRegistration.getScopes()); + assertThat(authorizationRequest.getState()).isNotNull(); + assertThat(authorizationRequest.getAdditionalParameters()) + .containsExactly(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId())); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-1"); + } + + @Test + public void resolveWhenClientAuthorizationRequiredExceptionAvailableThenResolves() { + ClientRegistration clientRegistration = this.registration2; + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.setAttribute( + OAuth2AuthorizationRequestRedirectFilter.AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME, + new ClientAuthorizationRequiredException(clientRegistration.getRegistrationId())); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest).isNotNull(); + assertThat(authorizationRequest.getAdditionalParameters()) + .containsExactly(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId())); + } + + @Test + public void resolveWhenAuthorizationRequestRedirectUriTemplatedThenRedirectUriExpanded() { + ClientRegistration clientRegistration = this.registration2; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getRedirectUri()).isNotEqualTo( + clientRegistration.getRedirectUriTemplate()); + assertThat(authorizationRequest.getRedirectUri()).isEqualTo( + "http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId()); + } + + // gh-5520 + @Test + public void resolveWhenAuthorizationRequestRedirectUriTemplatedThenRedirectUriExpandedExcludesQueryString() { + ClientRegistration clientRegistration = this.registration2; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.setQueryString("foo=bar"); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getRedirectUri()).isNotEqualTo( + clientRegistration.getRedirectUriTemplate()); + assertThat(authorizationRequest.getRedirectUri()).isEqualTo( + "http://localhost/login/oauth2/code/" + clientRegistration.getRegistrationId()); + } + + @Test + public void resolveWhenAuthorizationRequestIncludesPort80ThenExpandedRedirectUriExcludesPort() { + ClientRegistration clientRegistration = this.registration1; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setScheme("http"); + request.setServerName("example.com"); + request.setServerPort(80); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Fexample.com%2Flogin%2Foauth2%2Fcode%2Fregistration-1"); + } + + @Test + public void resolveWhenAuthorizationRequestIncludesPort443ThenExpandedRedirectUriExcludesPort() { + ClientRegistration clientRegistration = this.registration1; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setScheme("https"); + request.setServerName("example.com"); + request.setServerPort(443); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=https%3A%2F%2Fexample.com%2Flogin%2Foauth2%2Fcode%2Fregistration-1"); + } + + @Test + public void resolveWhenClientAuthorizationRequiredExceptionAvailableThenRedirectUriIsAuthorize() { + ClientRegistration clientRegistration = this.registration1; + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.setAttribute( + OAuth2AuthorizationRequestRedirectFilter.AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME, + new ClientAuthorizationRequiredException(clientRegistration.getRegistrationId())); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fcode%2Fregistration-1"); + } + + @Test + public void resolveWhenAuthorizationRequestOAuth2LoginThenRedirectUriIsLogin() { + ClientRegistration clientRegistration = this.registration2; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-2&scope=openid\\+profile\\+email&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-2"); + } + + @Test + public void resolveWhenAuthorizationRequestHasActionParameterAuthorizeThenRedirectUriIsAuthorize() { + ClientRegistration clientRegistration = this.registration1; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.addParameter("action", "authorize"); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fcode%2Fregistration-1"); + } + + @Test + public void resolveWhenAuthorizationRequestHasActionParameterLoginThenRedirectUriIsLogin() { + ClientRegistration clientRegistration = this.registration2; + String requestUri = this.authorizationRequestBaseUri + "/" + clientRegistration.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.addParameter("action", "login"); + request.setServletPath(requestUri); + + OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); + assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-2&scope=openid\\+profile\\+email&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-2"); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepositoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepositoryTests.java index 3bb2ed3244b..ace3225af17 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepositoryTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepositoryTests.java @@ -217,9 +217,16 @@ public void saveAuthorizationRequestWhenNullThenRemoved() { assertThat(loadedAuthorizationRequest).isNull(); } - @Test(expected = IllegalArgumentException.class) + @Test public void removeAuthorizationRequestWhenHttpServletRequestIsNullThenThrowIllegalArgumentException() { - this.authorizationRequestRepository.removeAuthorizationRequest(null); + assertThatThrownBy(() -> this.authorizationRequestRepository.removeAuthorizationRequest( + null, new MockHttpServletResponse())).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void removeAuthorizationRequestWhenHttpServletResponseIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizationRequestRepository.removeAuthorizationRequest( + new MockHttpServletRequest(), null)).isInstanceOf(IllegalArgumentException.class); } @Test @@ -234,7 +241,7 @@ public void removeAuthorizationRequestWhenSavedThenRemoved() { request.addParameter(OAuth2ParameterNames.STATE, authorizationRequest.getState()); OAuth2AuthorizationRequest removedAuthorizationRequest = - this.authorizationRequestRepository.removeAuthorizationRequest(request); + this.authorizationRequestRepository.removeAuthorizationRequest(request, response); OAuth2AuthorizationRequest loadedAuthorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request); @@ -255,7 +262,7 @@ public void removeAuthorizationRequestWhenSavedThenRemovedFromSession() { request.addParameter(OAuth2ParameterNames.STATE, authorizationRequest.getState()); OAuth2AuthorizationRequest removedAuthorizationRequest = - this.authorizationRequestRepository.removeAuthorizationRequest(request); + this.authorizationRequestRepository.removeAuthorizationRequest(request, response); String sessionAttributeName = HttpSessionOAuth2AuthorizationRequestRepository.class.getName() + ".AUTHORIZATION_REQUEST"; @@ -269,8 +276,10 @@ public void removeAuthorizationRequestWhenNotSavedThenNotRemoved() { MockHttpServletRequest request = new MockHttpServletRequest(); request.addParameter(OAuth2ParameterNames.STATE, "state-1234"); + MockHttpServletResponse response = new MockHttpServletResponse(); + OAuth2AuthorizationRequest removedAuthorizationRequest = - this.authorizationRequestRepository.removeAuthorizationRequest(request); + this.authorizationRequestRepository.removeAuthorizationRequest(request, response); assertThat(removedAuthorizationRequest).isNull(); } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepositoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepositoryTests.java new file mode 100644 index 00000000000..1578dd2cdf8 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepositoryTests.java @@ -0,0 +1,261 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; + +import javax.servlet.http.HttpSession; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HttpSessionOAuth2AuthorizedClientRepository}. + * + * @author Joe Grandja + */ +public class HttpSessionOAuth2AuthorizedClientRepositoryTests { + private String registrationId1 = "registration-1"; + private String registrationId2 = "registration-2"; + private String principalName1 = "principalName-1"; + + private ClientRegistration registration1 = ClientRegistration.withRegistrationId(this.registrationId1) + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("user") + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .userInfoUri("https://provider.com/oauth2/user") + .userNameAttributeName("id") + .clientName("client-1") + .build(); + + private ClientRegistration registration2 = ClientRegistration.withRegistrationId(this.registrationId2) + .clientId("client-2") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email") + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .userInfoUri("https://provider.com/oauth2/userinfo") + .jwkSetUri("https://provider.com/oauth2/keys") + .clientName("client-2") + .build(); + + private HttpSessionOAuth2AuthorizedClientRepository authorizedClientRepository = + new HttpSessionOAuth2AuthorizedClientRepository(); + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @Before + public void setup() { + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + } + + @Test + public void loadAuthorizedClientWhenClientRegistrationIdIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizedClientRepository.loadAuthorizedClient(null, null, this.request)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void loadAuthorizedClientWhenPrincipalNameIsNullThenExceptionNotThrown() { + this.authorizedClientRepository.loadAuthorizedClient(this.registrationId1, null, this.request); + } + + @Test + public void loadAuthorizedClientWhenRequestIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizedClientRepository.loadAuthorizedClient(this.registrationId1, null, null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void loadAuthorizedClientWhenClientRegistrationNotFoundThenReturnNull() { + OAuth2AuthorizedClient authorizedClient = + this.authorizedClientRepository.loadAuthorizedClient("registration-not-found", null, this.request); + assertThat(authorizedClient).isNull(); + } + + @Test + public void loadAuthorizedClientWhenSavedThenReturnAuthorizedClient() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration1, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, this.request, this.response); + + OAuth2AuthorizedClient loadedAuthorizedClient = + this.authorizedClientRepository.loadAuthorizedClient(this.registrationId1, null, this.request); + assertThat(loadedAuthorizedClient).isEqualTo(authorizedClient); + } + + @Test + public void saveAuthorizedClientWhenAuthorizedClientIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizedClientRepository.saveAuthorizedClient(null, null, this.request, this.response)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void saveAuthorizedClientWhenAuthenticationIsNullThenExceptionNotThrown() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration2, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, this.request, this.response); + } + + @Test + public void saveAuthorizedClientWhenRequestIsNullThenThrowIllegalArgumentException() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration2, this.principalName1, mock(OAuth2AccessToken.class)); + assertThatThrownBy(() -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, null, this.response)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void saveAuthorizedClientWhenResponseIsNullThenThrowIllegalArgumentException() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration2, this.principalName1, mock(OAuth2AccessToken.class)); + assertThatThrownBy(() -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, this.request, null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void saveAuthorizedClientWhenSavedThenSavedToSession() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration2, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, this.request, this.response); + + HttpSession session = this.request.getSession(false); + assertThat(session).isNotNull(); + + @SuppressWarnings("unchecked") + Map authorizedClients = (Map) + session.getAttribute(HttpSessionOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS"); + assertThat(authorizedClients).isNotEmpty(); + assertThat(authorizedClients).hasSize(1); + assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient); + } + + @Test + public void removeAuthorizedClientWhenClientRegistrationIdIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizedClientRepository.removeAuthorizedClient( + null, null, this.request, this.response)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void removeAuthorizedClientWhenPrincipalNameIsNullThenExceptionNotThrown() { + this.authorizedClientRepository.removeAuthorizedClient(this.registrationId1, null, this.request, this.response); + } + + @Test + public void removeAuthorizedClientWhenRequestIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authorizedClientRepository.removeAuthorizedClient( + this.registrationId1, null, null, this.response)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void removeAuthorizedClientWhenResponseIsNullThenExceptionNotThrown() { + this.authorizedClientRepository.removeAuthorizedClient(this.registrationId1, null, this.request, null); + } + + @Test + public void removeAuthorizedClientWhenNotSavedThenSessionNotCreated() { + this.authorizedClientRepository.removeAuthorizedClient( + this.registrationId2, null, this.request, this.response); + assertThat(this.request.getSession(false)).isNull(); + } + + @Test + public void removeAuthorizedClientWhenClient1SavedAndClient2RemovedThenClient1NotRemoved() { + OAuth2AuthorizedClient authorizedClient1 = new OAuth2AuthorizedClient( + this.registration1, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient1, null, this.request, this.response); + + // Remove registrationId2 (never added so is not removed either) + this.authorizedClientRepository.removeAuthorizedClient( + this.registrationId2, null, this.request, this.response); + + OAuth2AuthorizedClient loadedAuthorizedClient1 = this.authorizedClientRepository.loadAuthorizedClient( + this.registrationId1, null, this.request); + assertThat(loadedAuthorizedClient1).isNotNull(); + assertThat(loadedAuthorizedClient1).isSameAs(authorizedClient1); + } + + @Test + public void removeAuthorizedClientWhenSavedThenRemoved() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration2, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, this.request, this.response); + OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + this.registrationId2, null, this.request); + assertThat(loadedAuthorizedClient).isSameAs(authorizedClient); + this.authorizedClientRepository.removeAuthorizedClient( + this.registrationId2, null, this.request, this.response); + loadedAuthorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + this.registrationId2, null, this.request); + assertThat(loadedAuthorizedClient).isNull(); + } + + @Test + public void removeAuthorizedClientWhenSavedThenRemovedFromSession() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + this.registration1, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, null, this.request, this.response); + OAuth2AuthorizedClient loadedAuthorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + this.registrationId1, null, this.request); + assertThat(loadedAuthorizedClient).isSameAs(authorizedClient); + this.authorizedClientRepository.removeAuthorizedClient( + this.registrationId1, null, this.request, this.response); + + HttpSession session = this.request.getSession(false); + assertThat(session).isNotNull(); + assertThat(session.getAttribute(HttpSessionOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS")).isNull(); + } + + @Test + public void removeAuthorizedClientWhenClient1Client2SavedAndClient1RemovedThenClient2NotRemoved() { + OAuth2AuthorizedClient authorizedClient1 = new OAuth2AuthorizedClient( + this.registration1, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient1, null, this.request, this.response); + + OAuth2AuthorizedClient authorizedClient2 = new OAuth2AuthorizedClient( + this.registration2, this.principalName1, mock(OAuth2AccessToken.class)); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient2, null, this.request, this.response); + + this.authorizedClientRepository.removeAuthorizedClient( + this.registrationId1, null, this.request, this.response); + + OAuth2AuthorizedClient loadedAuthorizedClient2 = this.authorizedClientRepository.loadAuthorizedClient( + this.registrationId2, null, this.request); + assertThat(loadedAuthorizedClient2).isNotNull(); + assertThat(loadedAuthorizedClient2).isSameAs(authorizedClient2); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java index 6678d6e8b90..fa834f919ea 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java @@ -15,6 +15,7 @@ */ package org.springframework.security.oauth2.client.web; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -23,9 +24,11 @@ import org.powermock.modules.junit4.PowerMockRunner; 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.TestingAuthenticationToken; 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.oauth2.client.InMemoryOAuth2AuthorizedClientService; @@ -41,6 +44,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -50,6 +54,7 @@ import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import java.util.HashMap; import java.util.Map; @@ -70,12 +75,13 @@ public class OAuth2AuthorizationCodeGrantFilterTests { private String principalName1 = "principal-1"; private ClientRegistrationRepository clientRegistrationRepository; private OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository authorizedClientRepository; private AuthenticationManager authenticationManager; private AuthorizationRequestRepository authorizationRequestRepository; private OAuth2AuthorizationCodeGrantFilter filter; @Before - public void setUp() { + public void setup() { this.registration1 = ClientRegistration.withRegistrationId("registration-1") .clientId("client-1") .clientSecret("secret") @@ -91,32 +97,39 @@ public void setUp() { .build(); this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1); this.authorizedClientService = new InMemoryOAuth2AuthorizedClientService(this.clientRegistrationRepository); + this.authorizedClientRepository = new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(this.authorizedClientService); this.authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); this.authenticationManager = mock(AuthenticationManager.class); this.filter = spy(new OAuth2AuthorizationCodeGrantFilter( - this.clientRegistrationRepository, this.authorizedClientService, this.authenticationManager)); + this.clientRegistrationRepository, this.authorizedClientRepository, this.authenticationManager)); this.filter.setAuthorizationRequestRepository(this.authorizationRequestRepository); - + TestingAuthenticationToken authentication = new TestingAuthenticationToken(this.principalName1, "password"); + authentication.setAuthenticated(true); SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(new TestingAuthenticationToken(this.principalName1, "password")); + securityContext.setAuthentication(authentication); SecurityContextHolder.setContext(securityContext); } + @After + public void cleanup() { + SecurityContextHolder.clearContext(); + } + @Test public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(null, this.authorizedClientService, this.authenticationManager)) + assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(null, this.authorizedClientRepository, this.authenticationManager)) .isInstanceOf(IllegalArgumentException.class); } @Test - public void constructorWhenAuthorizedClientServiceIsNullThenThrowIllegalArgumentException() { + public void constructorWhenAuthorizedClientRepositoryIsNullThenThrowIllegalArgumentException() { assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(this.clientRegistrationRepository, null, this.authenticationManager)) .isInstanceOf(IllegalArgumentException.class); } @Test public void constructorWhenAuthenticationManagerIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(this.clientRegistrationRepository, this.authorizedClientService, null)) + assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(this.clientRegistrationRepository, this.authorizedClientRepository, null)) .isInstanceOf(IllegalArgumentException.class); } @@ -217,7 +230,7 @@ public void doFilterWhenAuthenticationFailsThenHandleOAuth2AuthenticationExcepti } @Test - public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSaved() throws Exception { + public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSavedToService() throws Exception { String requestUri = "/callback/client-1"; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); @@ -238,6 +251,7 @@ public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSaved() assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principalName1); assertThat(authorizedClient.getAccessToken()).isNotNull(); + assertThat(authorizedClient.getRefreshToken()).isNotNull(); } @Test @@ -283,6 +297,85 @@ public void doFilterWhenAuthorizationResponseSuccessHasSavedRequestThenRedirecte assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/saved-request"); } + @Test + public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception { + AnonymousAuthenticationToken anonymousPrincipal = + new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(anonymousPrincipal); + SecurityContextHolder.setContext(securityContext); + + String requestUri = "/callback/client-1"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(OAuth2ParameterNames.STATE, "state"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.setUpAuthorizationRequest(request, response, this.registration1); + this.setUpAuthenticationResult(this.registration1); + + this.filter.doFilter(request, response, filterChain); + + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + this.registration1.getRegistrationId(), anonymousPrincipal, request); + assertThat(authorizedClient).isNotNull(); + + assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); + assertThat(authorizedClient.getPrincipalName()).isEqualTo(anonymousPrincipal.getName()); + assertThat(authorizedClient.getAccessToken()).isNotNull(); + + HttpSession session = request.getSession(false); + assertThat(session).isNotNull(); + + @SuppressWarnings("unchecked") + Map authorizedClients = (Map) + session.getAttribute(HttpSessionOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS"); + assertThat(authorizedClients).isNotEmpty(); + assertThat(authorizedClients).hasSize(1); + assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient); + } + + @Test + public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + SecurityContextHolder.setContext(securityContext); // null Authentication + + String requestUri = "/callback/client-1"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(OAuth2ParameterNames.STATE, "state"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.setUpAuthorizationRequest(request, response, this.registration1); + this.setUpAuthenticationResult(this.registration1); + + this.filter.doFilter(request, response, filterChain); + + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + this.registration1.getRegistrationId(), null, request); + assertThat(authorizedClient).isNotNull(); + + assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); + assertThat(authorizedClient.getPrincipalName()).isEqualTo("anonymousUser"); + assertThat(authorizedClient.getAccessToken()).isNotNull(); + + HttpSession session = request.getSession(false); + assertThat(session).isNotNull(); + + @SuppressWarnings("unchecked") + Map authorizedClients = (Map) + session.getAttribute(HttpSessionOAuth2AuthorizedClientRepository.class.getName() + ".AUTHORIZED_CLIENTS"); + assertThat(authorizedClients).isNotEmpty(); + assertThat(authorizedClients).hasSize(1); + assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient); + } + private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response, ClientRegistration registration) { Map additionalParameters = new HashMap<>(); @@ -299,6 +392,7 @@ private void setUpAuthenticationResult(ClientRegistration registration) { when(authentication.getClientRegistration()).thenReturn(registration); when(authentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class)); when(authentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class)); + when(authentication.getRefreshToken()).thenReturn(mock(OAuth2RefreshToken.class)); when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java index 3412c852d10..b8dadbaf728 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -17,7 +17,6 @@ import org.junit.Before; import org.junit.Test; -import org.mockito.ArgumentCaptor; import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -29,16 +28,22 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.util.ClassUtils; +import org.springframework.web.util.UriComponentsBuilder; import javax.servlet.FilterChain; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Constructor; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.*; +import static org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME; /** * Tests for {@link OAuth2AuthorizationRequestRedirectFilter}. @@ -100,7 +105,9 @@ public void setUp() { @Test public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2AuthorizationRequestRedirectFilter(null)) + Constructor constructor = ClassUtils.getConstructorIfAvailable( + OAuth2AuthorizationRequestRedirectFilter.class, ClientRegistrationRepository.class); + assertThatThrownBy(() -> constructor.newInstance(null)) .isInstanceOf(IllegalArgumentException.class); } @@ -110,6 +117,14 @@ public void constructorWhenAuthorizationRequestBaseUriIsNullThenThrowIllegalArgu .isInstanceOf(IllegalArgumentException.class); } + @Test + public void constructorWhenAuthorizationRequestResolverIsNullThenThrowIllegalArgumentException() { + Constructor constructor = ClassUtils.getConstructorIfAvailable( + OAuth2AuthorizationRequestRedirectFilter.class, OAuth2AuthorizationRequestResolver.class); + assertThatThrownBy(() -> constructor.newInstance(null)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void setAuthorizationRequestRepositoryWhenAuthorizationRequestRepositoryIsNullThenThrowIllegalArgumentException() { assertThatThrownBy(() -> this.filter.setAuthorizationRequestRepository(null)) @@ -165,7 +180,7 @@ public void doFilterWhenAuthorizationRequestOAuth2LoginThenRedirectForAuthorizat verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/login/oauth2/code/registration-1"); + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-1"); } @Test @@ -201,7 +216,7 @@ public void doFilterWhenAuthorizationRequestImplicitGrantThenRedirectForAuthoriz verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=token&client_id=client-3&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/implicit/registration-3"); + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=token&client_id=client-3&scope=openid\\+profile\\+email&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fimplicit%2Fregistration-3"); } @Test @@ -239,75 +254,7 @@ public void doFilterWhenCustomAuthorizationRequestBaseUriThenRedirectForAuthoriz verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/login/oauth2/code/registration-1"); - } - - @Test - public void doFilterWhenAuthorizationRequestRedirectUriTemplatedThenRedirectUriExpanded() throws Exception { - String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + - "/" + this.registration2.getRegistrationId(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); - request.setServletPath(requestUri); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain filterChain = mock(FilterChain.class); - - AuthorizationRequestRepository authorizationRequestRepository = - mock(AuthorizationRequestRepository.class); - this.filter.setAuthorizationRequestRepository(authorizationRequestRepository); - - this.filter.doFilter(request, response, filterChain); - - ArgumentCaptor authorizationRequestArgCaptor = - ArgumentCaptor.forClass(OAuth2AuthorizationRequest.class); - - verifyZeroInteractions(filterChain); - verify(authorizationRequestRepository).saveAuthorizationRequest( - authorizationRequestArgCaptor.capture(), any(HttpServletRequest.class), any(HttpServletResponse.class)); - - OAuth2AuthorizationRequest authorizationRequest = authorizationRequestArgCaptor.getValue(); - - assertThat(authorizationRequest.getRedirectUri()).isNotEqualTo( - this.registration2.getRedirectUriTemplate()); - assertThat(authorizationRequest.getRedirectUri()).isEqualTo( - "http://localhost/login/oauth2/code/registration-2"); - } - - @Test - public void doFilterWhenAuthorizationRequestIncludesPort80ThenExpandedRedirectUriExcludesPort() throws Exception { - String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + - "/" + this.registration1.getRegistrationId(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); - request.setScheme("http"); - request.setServerName("example.com"); - request.setServerPort(80); - request.setServletPath(requestUri); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain filterChain = mock(FilterChain.class); - - this.filter.doFilter(request, response, filterChain); - - verifyZeroInteractions(filterChain); - - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://example.com/login/oauth2/code/registration-1"); - } - - @Test - public void doFilterWhenAuthorizationRequestIncludesPort443ThenExpandedRedirectUriExcludesPort() throws Exception { - String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + - "/" + this.registration1.getRegistrationId(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); - request.setScheme("https"); - request.setServerName("example.com"); - request.setServerPort(443); - request.setServletPath(requestUri); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain filterChain = mock(FilterChain.class); - - this.filter.doFilter(request, response, filterChain); - - verifyZeroInteractions(filterChain); - - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=https://example.com/login/oauth2/code/registration-1"); + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-1"); } @Test @@ -325,13 +272,13 @@ public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExc verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/code/registration-1"); - + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fcode%2Fregistration-1"); verify(this.requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class)); + assertThat(request.getAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME)).isNull(); } @Test - public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExceptionThrownThenRedirectUriIsAuthorize() throws Exception { + public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExceptionThrownButAuthorizationRequestNotResolvedThenStatusInternalServerError() throws Exception { String requestUri = "/path"; MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); request.setServletPath(requestUri); @@ -341,60 +288,84 @@ public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExc doThrow(new ClientAuthorizationRequiredException(this.registration1.getRegistrationId())) .when(filterChain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); - this.filter.doFilter(request, response, filterChain); + OAuth2AuthorizationRequestResolver resolver = req -> null; + OAuth2AuthorizationRequestRedirectFilter filter = new OAuth2AuthorizationRequestRedirectFilter(resolver); - verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + filter.doFilter(request, response, filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/code/registration-1"); - } - - @Test - public void doFilterWhenAuthorizationRequestOAuth2LoginThenRedirectUriIsLogin() throws Exception { - String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + - "/" + this.registration2.getRegistrationId(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); - request.setServletPath(requestUri); - MockHttpServletResponse response = new MockHttpServletResponse(); - FilterChain filterChain = mock(FilterChain.class); - - this.filter.doFilter(request, response, filterChain); + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-2&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/login/oauth2/code/registration-2"); + assertThat(response.getStatus()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); + assertThat(response.getErrorMessage()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()); } + // gh-4911 @Test - public void doFilterWhenAuthorizationRequestHasActionParameterAuthorizeThenRedirectUriIsAuthorize() throws Exception { + public void doFilterWhenAuthorizationRequestAndAdditionalParametersProvidedThenAuthorizationRequestIncludesAdditionalParameters() throws Exception { String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + this.registration1.getRegistrationId(); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); - request.addParameter("action", "authorize"); request.setServletPath(requestUri); + request.addParameter("idp", "https://other.provider.com"); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); - this.filter.doFilter(request, response, filterChain); + OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( + this.clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); + + OAuth2AuthorizationRequestResolver resolver = req -> { + OAuth2AuthorizationRequest defaultAuthorizationRequest = defaultAuthorizationRequestResolver.resolve(req); + Map additionalParameters = new HashMap<>(defaultAuthorizationRequest.getAdditionalParameters()); + additionalParameters.put("idp", req.getParameter("idp")); + return OAuth2AuthorizationRequest.from(defaultAuthorizationRequest) + .additionalParameters(additionalParameters) + .build(); + }; + OAuth2AuthorizationRequestRedirectFilter filter = new OAuth2AuthorizationRequestRedirectFilter(resolver); + + filter.doFilter(request, response, filterChain); verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/code/registration-1"); + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-1&idp=https%3A%2F%2Fother.provider.com"); } + // gh-4911, gh-5244 @Test - public void doFilterWhenAuthorizationRequestHasActionParameterLoginThenRedirectUriIsLogin() throws Exception { + public void doFilterWhenAuthorizationRequestAndCustomAuthorizationRequestUriSetThenCustomAuthorizationRequestUriUsed() throws Exception { String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + - "/" + this.registration2.getRegistrationId(); + "/" + this.registration1.getRegistrationId(); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); - request.addParameter("action", "login"); request.setServletPath(requestUri); + String loginHintParamName = "login_hint"; + request.addParameter(loginHintParamName, "user@provider.com"); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = mock(FilterChain.class); - this.filter.doFilter(request, response, filterChain); + OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( + this.clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); + + OAuth2AuthorizationRequestResolver resolver = req -> { + OAuth2AuthorizationRequest defaultAuthorizationRequest = defaultAuthorizationRequestResolver.resolve(req); + Map additionalParameters = new HashMap<>(defaultAuthorizationRequest.getAdditionalParameters()); + additionalParameters.put(loginHintParamName, req.getParameter(loginHintParamName)); + String customAuthorizationRequestUri = UriComponentsBuilder + .fromUriString(defaultAuthorizationRequest.getAuthorizationRequestUri()) + .queryParam(loginHintParamName, additionalParameters.get(loginHintParamName)) + .build(true).toUriString(); + return OAuth2AuthorizationRequest.from(defaultAuthorizationRequest) + .additionalParameters(additionalParameters) + .authorizationRequestUri(customAuthorizationRequestUri) + .build(); + }; + OAuth2AuthorizationRequestRedirectFilter filter = new OAuth2AuthorizationRequestRedirectFilter(resolver); + + filter.doFilter(request, response, filterChain); verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-2&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/login/oauth2/code/registration-2"); + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-1&login_hint=user@provider\\.com"); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java index 82fc51de2ae..b2ff790cbe4 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectWebFilterTests.java @@ -21,6 +21,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -133,4 +134,35 @@ public void filterWhenDoesMatchThenClientRegistrationRepositoryNotSubscribed() { }); verify(this.authzRequestRepository).saveAuthorizationRequest(any(), any()); } + + // gh-5520 + @Test + public void filterWhenDoesMatchThenResolveRedirectUriExpandedExcludesQueryString() { + FluxExchangeResult result = this.client.get() + .uri("https://example.com/oauth2/authorization/github?foo=bar").exchange() + .expectStatus().is3xxRedirection().returnResult(String.class); + result.assertWithDiagnostics(() -> { + URI location = result.getResponseHeaders().getLocation(); + assertThat(location) + .hasScheme("https") + .hasHost("github.com") + .hasPath("/login/oauth/authorize") + .hasParameter("response_type", "code") + .hasParameter("client_id", "clientId") + .hasParameter("scope", "read:user") + .hasParameter("state") + .hasParameter("redirect_uri", "https://example.com/login/oauth2/code/github"); + }); + } + + @Test + public void filterWhenExceptionThenRedirected() { + FilteringWebHandler webHandler = new FilteringWebHandler(e -> Mono.error(new ClientAuthorizationRequiredException(this.github.getRegistrationId())), Arrays.asList(this.filter)); + this.client = WebTestClient.bindToWebHandler(webHandler).build(); + FluxExchangeResult result = this.client.get() + .uri("https://example.com/foo").exchange() + .expectStatus() + .is3xxRedirection() + .returnResult(String.class); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilderTests.java deleted file mode 100644 index 0fde656c870..00000000000 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestUriBuilderTests.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://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.web; - -import org.junit.Test; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; - -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link OAuth2AuthorizationRequestUriBuilder}. - * - * @author Rob Winch - * @since 5.0 - */ -public class OAuth2AuthorizationRequestUriBuilderTests { - private OAuth2AuthorizationRequestUriBuilder builder = new OAuth2AuthorizationRequestUriBuilder(); - - @Test(expected = IllegalArgumentException.class) - public void buildWhenAuthorizationRequestIsNullThenThrowIllegalArgumentException() { - this.builder.build(null); - } - - @Test - public void buildWhenScopeMultiThenSeparatedByEncodedSpace() { - OAuth2AuthorizationRequest request = OAuth2AuthorizationRequest.implicit() - .additionalParameters(Collections.singletonMap("foo", "bar")) - .authorizationUri("https://idp.example.com/oauth2/v2/auth") - .clientId("client-id") - .state("thestate") - .redirectUri("https://client.example.com/login/oauth2") - .scopes(new HashSet<>(Arrays.asList("openid", "user"))) - .build(); - - URI result = this.builder.build(request); - - assertThat(result.toASCIIString()).isEqualTo("https://idp.example.com/oauth2/v2/auth?response_type=token&client_id=client-id&scope=openid%20user&state=thestate&redirect_uri=https://client.example.com/login/oauth2"); - } -} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java index 2b788c82456..1546a5b396d 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -40,6 +40,7 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -69,10 +70,12 @@ public class OAuth2LoginAuthenticationFilterTests { private ClientRegistration registration2; private String principalName1 = "principal-1"; private ClientRegistrationRepository clientRegistrationRepository; + private OAuth2AuthorizedClientRepository authorizedClientRepository; private OAuth2AuthorizedClientService authorizedClientService; private AuthorizationRequestRepository authorizationRequestRepository; private AuthenticationFailureHandler failureHandler; private AuthenticationManager authenticationManager; + private OAuth2LoginAuthenticationToken loginAuthentication; private OAuth2LoginAuthenticationFilter filter; @Before @@ -106,11 +109,12 @@ public void setUp() { this.clientRegistrationRepository = new InMemoryClientRegistrationRepository( this.registration1, this.registration2); this.authorizedClientService = new InMemoryOAuth2AuthorizedClientService(this.clientRegistrationRepository); + this.authorizedClientRepository = new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(this.authorizedClientService); this.authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); this.failureHandler = mock(AuthenticationFailureHandler.class); this.authenticationManager = mock(AuthenticationManager.class); - this.filter = spy(new OAuth2LoginAuthenticationFilter( - this.clientRegistrationRepository, this.authorizedClientService)); + this.filter = spy(new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, + this.authorizedClientRepository, OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI)); this.filter.setAuthorizationRequestRepository(this.authorizationRequestRepository); this.filter.setAuthenticationFailureHandler(this.failureHandler); this.filter.setAuthenticationManager(this.authenticationManager); @@ -128,9 +132,16 @@ public void constructorWhenAuthorizedClientServiceIsNullThenThrowIllegalArgument .isInstanceOf(IllegalArgumentException.class); } + @Test + public void constructorWhenAuthorizedClientRepositoryIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, + (OAuth2AuthorizedClientRepository) null, OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void constructorWhenFilterProcessesUrlIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, this.authorizedClientService, null)) + assertThatThrownBy(() -> new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, this.authorizedClientRepository, null)) .isInstanceOf(IllegalArgumentException.class); } @@ -275,19 +286,20 @@ public void doFilterWhenAuthorizationResponseValidThenAuthorizedClientSaved() th this.filter.doFilter(request, response, filterChain); - OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient( - this.registration1.getRegistrationId(), this.principalName1); + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + this.registration1.getRegistrationId(), this.loginAuthentication, request); assertThat(authorizedClient).isNotNull(); assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principalName1); assertThat(authorizedClient.getAccessToken()).isNotNull(); + assertThat(authorizedClient.getRefreshToken()).isNotNull(); } @Test public void doFilterWhenCustomFilterProcessesUrlThenFilterProcesses() throws Exception { String filterProcessesUrl = "/login/oauth2/custom/*"; this.filter = spy(new OAuth2LoginAuthenticationFilter( - this.clientRegistrationRepository, this.authorizedClientService, filterProcessesUrl)); + this.clientRegistrationRepository, this.authorizedClientRepository, filterProcessesUrl)); this.filter.setAuthenticationManager(this.authenticationManager); String requestUri = "/login/oauth2/custom/" + this.registration2.getRegistrationId(); @@ -322,12 +334,15 @@ private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletRe private void setUpAuthenticationResult(ClientRegistration registration) { OAuth2User user = mock(OAuth2User.class); when(user.getName()).thenReturn(this.principalName1); - OAuth2LoginAuthenticationToken loginAuthentication = mock(OAuth2LoginAuthenticationToken.class); - when(loginAuthentication.getPrincipal()).thenReturn(user); - when(loginAuthentication.getAuthorities()).thenReturn(AuthorityUtils.createAuthorityList("ROLE_USER")); - when(loginAuthentication.getClientRegistration()).thenReturn(registration); - when(loginAuthentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class)); - when(loginAuthentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class)); - when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(loginAuthentication); + this.loginAuthentication = mock(OAuth2LoginAuthenticationToken.class); + when(this.loginAuthentication.getPrincipal()).thenReturn(user); + when(this.loginAuthentication.getName()).thenReturn(this.principalName1); + when(this.loginAuthentication.getAuthorities()).thenReturn(AuthorityUtils.createAuthorityList("ROLE_USER")); + when(this.loginAuthentication.getClientRegistration()).thenReturn(registration); + when(this.loginAuthentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class)); + when(this.loginAuthentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class)); + when(this.loginAuthentication.getRefreshToken()).thenReturn(mock(OAuth2RefreshToken.class)); + when(this.loginAuthentication.isAuthenticated()).thenReturn(true); + when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(this.loginAuthentication); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverterTest.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverterTest.java index 8ceb5b17275..ac20d8a508e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverterTest.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/ServerOAuth2LoginAuthenticationTokenConverterTest.java @@ -141,6 +141,6 @@ public void applyWhenCodeParameterFoundThenCode() { private OAuth2LoginAuthenticationToken applyConverter() { MockServerWebExchange exchange = MockServerWebExchange.from(this.request); - return (OAuth2LoginAuthenticationToken) this.converter.apply(exchange).block(); + return (OAuth2LoginAuthenticationToken) this.converter.convert(exchange).block(); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java index c4fc7adaedb..142fc0a8da1 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/WebSessionOAuth2ReactiveAuthorizationRequestRepositoryTests.java @@ -179,6 +179,23 @@ public void removeAuthorizationRequestWhenPresentThenFoundAndRemoved() { .verifyComplete(); } + // gh-5599 + @Test + public void removeAuthorizationRequestWhenStateMissingThenNoErrors() { + MockServerHttpRequest otherState = MockServerHttpRequest.get("/") + .queryParam(OAuth2ParameterNames.STATE, "other") + .build(); + ServerWebExchange otherStateExchange = this.exchange.mutate() + .request(otherState) + .build(); + Mono saveAndRemove = this.repository + .saveAuthorizationRequest(this.authorizationRequest, this.exchange) + .then(this.repository.removeAuthorizationRequest(otherStateExchange)); + + StepVerifier.create(saveAndRemove) + .verifyComplete(); + } + @Test public void removeAuthorizationRequestWhenMultipleThenOnlyOneRemoved() { String oldState = "state0"; diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java new file mode 100644 index 00000000000..b49a1b2c01a --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java @@ -0,0 +1,260 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.method.annotation; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; +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.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.context.request.ServletWebRequest; + +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.Method; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * Tests for {@link OAuth2AuthorizedClientArgumentResolver}. + * + * @author Joe Grandja + */ +public class OAuth2AuthorizedClientArgumentResolverTests { + private TestingAuthenticationToken authentication; + private String principalName = "principal-1"; + private ClientRegistration registration1; + private ClientRegistration registration2; + private ClientRegistrationRepository clientRegistrationRepository; + private OAuth2AuthorizedClient authorizedClient1; + private OAuth2AuthorizedClient authorizedClient2; + private OAuth2AuthorizedClientRepository authorizedClientRepository; + private OAuth2AuthorizedClientArgumentResolver argumentResolver; + private MockHttpServletRequest request; + + @Before + public void setup() { + this.authentication = new TestingAuthenticationToken(this.principalName, "password"); + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(this.authentication); + SecurityContextHolder.setContext(securityContext); + + this.registration1 = ClientRegistration.withRegistrationId("client1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("user") + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .userInfoUri("https://provider.com/oauth2/user") + .userNameAttributeName("id") + .clientName("client-1") + .build(); + this.registration2 = ClientRegistration.withRegistrationId("client2") + .clientId("client-2") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope("read", "write") + .tokenUri("https://provider.com/oauth2/token") + .build(); + this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1, this.registration2); + this.authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class); + this.argumentResolver = new OAuth2AuthorizedClientArgumentResolver( + this.clientRegistrationRepository, this.authorizedClientRepository); + this.authorizedClient1 = new OAuth2AuthorizedClient(this.registration1, this.principalName, mock(OAuth2AccessToken.class)); + when(this.authorizedClientRepository.loadAuthorizedClient( + eq(this.registration1.getRegistrationId()), any(Authentication.class), any(HttpServletRequest.class))) + .thenReturn(this.authorizedClient1); + this.authorizedClient2 = new OAuth2AuthorizedClient(this.registration2, this.principalName, mock(OAuth2AccessToken.class)); + when(this.authorizedClientRepository.loadAuthorizedClient( + eq(this.registration2.getRegistrationId()), any(Authentication.class), any(HttpServletRequest.class))) + .thenReturn(this.authorizedClient2); + this.request = new MockHttpServletRequest(); + } + + @After + public void cleanup() { + SecurityContextHolder.clearContext(); + } + + @Test + public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizedClientArgumentResolver(null, this.authorizedClientRepository)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenOAuth2AuthorizedClientRepositoryIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizedClientArgumentResolver(this.clientRegistrationRepository, null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void setClientCredentialsTokenResponseClientWhenClientIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.argumentResolver.setClientCredentialsTokenResponseClient(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void supportsParameterWhenParameterTypeOAuth2AuthorizedClientThenTrue() { + MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); + assertThat(this.argumentResolver.supportsParameter(methodParameter)).isTrue(); + } + + @Test + public void supportsParameterWhenParameterTypeOAuth2AuthorizedClientWithoutAnnotationThenFalse() { + MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClientWithoutAnnotation", OAuth2AuthorizedClient.class); + assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); + } + + @Test + public void supportsParameterWhenParameterTypeUnsupportedThenFalse() { + MethodParameter methodParameter = this.getMethodParameter("paramTypeUnsupported", String.class); + assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); + } + + @Test + public void supportsParameterWhenParameterTypeUnsupportedWithoutAnnotationThenFalse() { + MethodParameter methodParameter = this.getMethodParameter("paramTypeUnsupportedWithoutAnnotation", String.class); + assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); + } + + @Test + public void resolveArgumentWhenRegistrationIdEmptyAndNotOAuth2AuthenticationThenThrowIllegalArgumentException() { + MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AuthorizedClient.class); + assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, null, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unable to resolve the Client Registration Identifier. It must be provided via @RegisteredOAuth2AuthorizedClient(\"client1\") or @RegisteredOAuth2AuthorizedClient(registrationId = \"client1\")."); + } + + @Test + public void resolveArgumentWhenRegistrationIdEmptyAndOAuth2AuthenticationThenResolves() throws Exception { + OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class); + when(authentication.getAuthorizedClientRegistrationId()).thenReturn("client1"); + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); + MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AuthorizedClient.class); + assertThat(this.argumentResolver.resolveArgument( + methodParameter, null, new ServletWebRequest(this.request), null)).isSameAs(this.authorizedClient1); + } + + @Test + public void resolveArgumentWhenAuthorizedClientFoundThenResolves() throws Exception { + MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); + assertThat(this.argumentResolver.resolveArgument( + methodParameter, null, new ServletWebRequest(this.request), null)).isSameAs(this.authorizedClient1); + } + + @Test + public void resolveArgumentWhenRegistrationIdInvalidThenDoesNotResolve() throws Exception { + MethodParameter methodParameter = this.getMethodParameter("registrationIdInvalid", OAuth2AuthorizedClient.class); + assertThat(this.argumentResolver.resolveArgument( + methodParameter, null, new ServletWebRequest(this.request), null)).isNull(); + } + + @Test + public void resolveArgumentWhenAuthorizedClientNotFoundForAuthorizationCodeClientThenThrowClientAuthorizationRequiredException() { + when(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(), any(HttpServletRequest.class))) + .thenReturn(null); + MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); + assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, new ServletWebRequest(this.request), null)) + .isInstanceOf(ClientAuthorizationRequiredException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void resolveArgumentWhenAuthorizedClientNotFoundForClientCredentialsClientThenResolvesFromTokenResponseClient() throws Exception { + OAuth2AccessTokenResponseClient clientCredentialsTokenResponseClient = + mock(OAuth2AccessTokenResponseClient.class); + this.argumentResolver.setClientCredentialsTokenResponseClient(clientCredentialsTokenResponseClient); + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse + .withToken("access-token-1234") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .build(); + when(clientCredentialsTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + when(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(), any(HttpServletRequest.class))) + .thenReturn(null); + MethodParameter methodParameter = this.getMethodParameter("clientCredentialsClient", OAuth2AuthorizedClient.class); + + OAuth2AuthorizedClient authorizedClient = (OAuth2AuthorizedClient) this.argumentResolver.resolveArgument( + methodParameter, null, new ServletWebRequest(this.request), null); + + assertThat(authorizedClient).isNotNull(); + assertThat(authorizedClient.getClientRegistration()).isSameAs(this.registration2); + assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principalName); + assertThat(authorizedClient.getAccessToken()).isSameAs(accessTokenResponse.getAccessToken()); + + verify(this.authorizedClientRepository).saveAuthorizedClient( + eq(authorizedClient), eq(this.authentication), any(HttpServletRequest.class), eq(null)); + } + + private MethodParameter getMethodParameter(String methodName, Class... paramTypes) { + Method method = ReflectionUtils.findMethod(TestController.class, methodName, paramTypes); + return new MethodParameter(method, 0); + } + + static class TestController { + void paramTypeAuthorizedClient(@RegisteredOAuth2AuthorizedClient("client1") OAuth2AuthorizedClient authorizedClient) { + } + + void paramTypeAuthorizedClientWithoutAnnotation(OAuth2AuthorizedClient authorizedClient) { + } + + void paramTypeUnsupported(@RegisteredOAuth2AuthorizedClient("client1") String param) { + } + + void paramTypeUnsupportedWithoutAnnotation(String param) { + } + + void registrationIdEmpty(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) { + } + + void registrationIdInvalid(@RegisteredOAuth2AuthorizedClient("invalid") OAuth2AuthorizedClient authorizedClient) { + } + + void clientCredentialsClient(@RegisteredOAuth2AuthorizedClient("client2") OAuth2AuthorizedClient authorizedClient) { + } + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolverTests.java deleted file mode 100644 index 5ad0fed1574..00000000000 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2ClientArgumentResolverTests.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright 2002-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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.web.method.annotation; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.core.MethodParameter; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.util.ReflectionUtils; - -import java.lang.reflect.Method; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * Tests for {@link OAuth2ClientArgumentResolver}. - * - * @author Joe Grandja - */ -public class OAuth2ClientArgumentResolverTests { - private ClientRegistrationRepository clientRegistrationRepository; - private OAuth2AuthorizedClientService authorizedClientService; - private OAuth2ClientArgumentResolver argumentResolver; - private ClientRegistration clientRegistration; - private OAuth2AuthorizedClient authorizedClient; - private OAuth2AccessToken accessToken; - - @Before - public void setUp() { - this.clientRegistrationRepository = mock(ClientRegistrationRepository.class); - this.authorizedClientService = mock(OAuth2AuthorizedClientService.class); - this.argumentResolver = new OAuth2ClientArgumentResolver( - this.clientRegistrationRepository, this.authorizedClientService); - this.clientRegistration = ClientRegistration.withRegistrationId("client1") - .clientId("client-id") - .clientSecret("secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate("{baseUrl}/client1") - .scope("scope1", "scope2") - .authorizationUri("https://provider.com/oauth2/auth") - .tokenUri("https://provider.com/oauth2/token") - .clientName("Client 1") - .build(); - when(this.clientRegistrationRepository.findByRegistrationId(anyString())).thenReturn(this.clientRegistration); - this.authorizedClient = mock(OAuth2AuthorizedClient.class); - when(this.authorizedClientService.loadAuthorizedClient(anyString(), any())).thenReturn(this.authorizedClient); - this.accessToken = mock(OAuth2AccessToken.class); - when(this.authorizedClient.getAccessToken()).thenReturn(this.accessToken); - SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(mock(Authentication.class)); - SecurityContextHolder.setContext(securityContext); - } - - @Test - public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientArgumentResolver(null, this.authorizedClientService)) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void constructorWhenOAuth2AuthorizedClientServiceIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientArgumentResolver(this.clientRegistrationRepository, null)) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void supportsParameterWhenParameterTypeOAuth2AccessTokenThenTrue() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessToken", OAuth2AccessToken.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isTrue(); - } - - @Test - public void supportsParameterWhenParameterTypeOAuth2AccessTokenWithoutAnnotationThenFalse() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessTokenWithoutAnnotation", OAuth2AccessToken.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); - } - - @Test - public void supportsParameterWhenParameterTypeOAuth2AuthorizedClientThenTrue() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isTrue(); - } - - @Test - public void supportsParameterWhenParameterTypeOAuth2AuthorizedClientWithoutAnnotationThenFalse() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClientWithoutAnnotation", OAuth2AuthorizedClient.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); - } - - @Test - public void supportsParameterWhenParameterTypeClientRegistrationThenTrue() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistration", ClientRegistration.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isTrue(); - } - - @Test - public void supportsParameterWhenParameterTypeClientRegistrationWithoutAnnotationThenFalse() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistrationWithoutAnnotation", ClientRegistration.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); - } - - @Test - public void supportsParameterWhenParameterTypeUnsupportedThenFalse() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeUnsupported", String.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); - } - - @Test - public void supportsParameterWhenParameterTypeUnsupportedWithoutAnnotationThenFalse() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeUnsupportedWithoutAnnotation", String.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); - } - - @Test - public void resolveArgumentWhenRegistrationIdEmptyAndNotOAuth2AuthenticationThenThrowIllegalArgumentException() throws Exception { - MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AccessToken.class); - assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, null, null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unable to resolve the Client Registration Identifier. It must be provided via @OAuth2Client(\"client1\") or @OAuth2Client(registrationId = \"client1\")."); - } - - @Test - public void resolveArgumentWhenRegistrationIdEmptyAndOAuth2AuthenticationThenResolves() throws Exception { - OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class); - when(authentication.getAuthorizedClientRegistrationId()).thenReturn("client1"); - SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(authentication); - SecurityContextHolder.setContext(securityContext); - MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AccessToken.class); - this.argumentResolver.resolveArgument(methodParameter, null, null, null); - } - - @Test - public void resolveArgumentWhenClientRegistrationFoundThenResolves() throws Exception { - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistration", ClientRegistration.class); - assertThat(this.argumentResolver.resolveArgument(methodParameter, null, null, null)).isSameAs(this.clientRegistration); - } - - @Test - public void resolveArgumentWhenClientRegistrationNotFoundThenThrowIllegalArgumentException() throws Exception { - when(this.clientRegistrationRepository.findByRegistrationId(anyString())).thenReturn(null); - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistration", ClientRegistration.class); - assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, null, null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unable to find ClientRegistration with registration identifier \"client1\"."); - } - - @Test - public void resolveArgumentWhenParameterTypeOAuth2AuthorizedClientAndCurrentAuthenticationNullThenThrowIllegalStateException() throws Exception { - SecurityContextHolder.clearContext(); - MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); - assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, null, null)) - .isInstanceOf(IllegalStateException.class) - .hasMessage("Unable to resolve the Authorized Client with registration identifier \"client1\". " + - "An \"authenticated\" or \"unauthenticated\" session is required. " + - "To allow for unauthenticated access, ensure HttpSecurity.anonymous() is configured."); - } - - @Test - public void resolveArgumentWhenOAuth2AuthorizedClientFoundThenResolves() throws Exception { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); - assertThat(this.argumentResolver.resolveArgument(methodParameter, null, null, null)).isSameAs(this.authorizedClient); - } - - @Test - public void resolveArgumentWhenOAuth2AuthorizedClientNotFoundThenThrowClientAuthorizationRequiredException() throws Exception { - when(this.authorizedClientService.loadAuthorizedClient(anyString(), any())).thenReturn(null); - MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); - assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, null, null)) - .isInstanceOf(ClientAuthorizationRequiredException.class); - } - - @Test - public void resolveArgumentWhenOAuth2AccessTokenAndOAuth2AuthorizedClientFoundThenResolves() throws Exception { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessToken", OAuth2AccessToken.class); - assertThat(this.argumentResolver.resolveArgument(methodParameter, null, null, null)).isSameAs(this.authorizedClient.getAccessToken()); - } - - @Test - public void resolveArgumentWhenOAuth2AccessTokenAndOAuth2AuthorizedClientNotFoundThenThrowClientAuthorizationRequiredException() throws Exception { - when(this.authorizedClientService.loadAuthorizedClient(anyString(), any())).thenReturn(null); - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessToken", OAuth2AccessToken.class); - assertThatThrownBy(() -> this.argumentResolver.resolveArgument(methodParameter, null, null, null)) - .isInstanceOf(ClientAuthorizationRequiredException.class); - } - - @Test - public void resolveArgumentWhenOAuth2AccessTokenAndAnnotationRegistrationIdSetThenResolves() throws Exception { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessTokenAnnotationRegistrationId", OAuth2AccessToken.class); - assertThat(this.argumentResolver.resolveArgument(methodParameter, null, null, null)).isSameAs(this.authorizedClient.getAccessToken()); - } - - private MethodParameter getMethodParameter(String methodName, Class... paramTypes) { - Method method = ReflectionUtils.findMethod(TestController.class, methodName, paramTypes); - return new MethodParameter(method, 0); - } - - static class TestController { - void paramTypeAccessToken(@OAuth2Client("client1") OAuth2AccessToken accessToken) { - } - - void paramTypeAccessTokenWithoutAnnotation(OAuth2AccessToken accessToken) { - } - - void paramTypeAuthorizedClient(@OAuth2Client("client1") OAuth2AuthorizedClient authorizedClient) { - } - - void paramTypeAuthorizedClientWithoutAnnotation(OAuth2AuthorizedClient authorizedClient) { - } - - void paramTypeClientRegistration(@OAuth2Client("client1") ClientRegistration clientRegistration) { - } - - void paramTypeClientRegistrationWithoutAnnotation(ClientRegistration clientRegistration) { - } - - void paramTypeUnsupported(@OAuth2Client("client1") String param) { - } - - void paramTypeUnsupportedWithoutAnnotation(String param) { - } - - void registrationIdEmpty(@OAuth2Client OAuth2AccessToken accessToken) { - } - - void paramTypeAccessTokenAnnotationRegistrationId(@OAuth2Client(registrationId = "client1") OAuth2AccessToken accessToken) { - } - } -} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java new file mode 100644 index 00000000000..8f1665d4377 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/MockExchangeFunction.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.reactive.function.client; + +import static org.mockito.Mockito.mock; + +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; + +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class MockExchangeFunction implements ExchangeFunction { + private List requests = new ArrayList<>(); + + private ClientResponse response = mock(ClientResponse.class); + + public ClientRequest getRequest() { + return this.requests.get(this.requests.size() - 1); + } + + public List getRequests() { + return this.requests; + } + + public ClientResponse getResponse() { + return this.response; + } + + @Override + public Mono exchange(ClientRequest request) { + return Mono.defer(() -> { + this.requests.add(request); + return Mono.just(this.response); + }); + } +} 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 new file mode 100644 index 00000000000..d19d1ee386f --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -0,0 +1,311 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.reactive.function.client; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.core.codec.ByteBufferEncoder; +import org.springframework.core.codec.CharSequenceEncoder; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.codec.EncoderHttpMessageWriter; +import org.springframework.http.codec.FormHttpMessageWriter; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.codec.ResourceHttpMessageWriter; +import org.springframework.http.codec.ServerSentEventHttpMessageWriter; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.mock.http.client.reactive.MockClientHttpRequest; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.reactive.function.client.ClientRequest; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests { + @Mock + private ReactiveOAuth2AuthorizedClientService authorizedClientService; + + private ServerOAuth2AuthorizedClientExchangeFilterFunction function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(); + + private MockExchangeFunction exchange = new MockExchangeFunction(); + + private ClientRegistration github = ClientRegistration.withRegistrationId("github") + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .scope("read:user") + .authorizationUri("https://github.com/login/oauth/authorize") + .tokenUri("https://github.com/login/oauth/access_token") + .userInfoUri("https://api.github.com/user") + .userNameAttributeName("id") + .clientName("GitHub") + .clientId("clientId") + .clientSecret("clientSecret") + .build(); + + private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "token-0", + Instant.now(), + Instant.now().plus(Duration.ofDays(1))); + + @Test + public void filterWhenAuthorizedClientNullThenAuthorizationHeaderNull() { + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .build(); + + this.function.filter(request, this.exchange).block(); + + assertThat(this.exchange.getRequest().headers().getFirst(HttpHeaders.AUTHORIZATION)).isNull(); + } + + @Test + public void filterWhenAuthorizedClientThenAuthorizationHeader() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + assertThat(this.exchange.getRequest().headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + @Test + public void filterWhenExistingAuthorizationThenSingleAuthorizationHeader() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .header(HttpHeaders.AUTHORIZATION, "Existing") + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + HttpHeaders headers = this.exchange.getRequest().headers(); + assertThat(headers.get(HttpHeaders.AUTHORIZATION)).containsOnly("Bearer " + this.accessToken.getTokenValue()); + } + + @Test + public void filterWhenRefreshRequiredThenRefresh() { + when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn(Mono.empty()); + OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .refreshToken("refresh-1") + .build(); + when(this.exchange.getResponse().body(any())).thenReturn(Mono.just(response)); + Instant issuedAt = Instant.now().minus(Duration.ofDays(1)); + Instant accessTokenExpiresAt = issuedAt.plus(Duration.ofHours(1)); + Instant refreshTokenExpiresAt = Instant.now().plus(Duration.ofHours(1)); + + this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), + this.accessToken.getTokenValue(), + issuedAt, + accessTokenExpiresAt); + this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); + + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt, refreshTokenExpiresAt); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken, refreshToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + TestingAuthenticationToken authentication = new TestingAuthenticationToken("test", "this"); + this.function.filter(request, this.exchange) + .subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication)) + .block(); + + verify(this.authorizedClientService).saveAuthorizedClient(any(), eq(authentication)); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(2); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://github.com/login/oauth/access_token"); + assertThat(request0.method()).isEqualTo(HttpMethod.POST); + assertThat(getBody(request0)).isEqualTo("grant_type=refresh_token&refresh_token=refresh-token"); + + ClientRequest request1 = requests.get(1); + assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-1"); + assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request1.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request1)).isEmpty(); + } + + @Test + public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() { + when(this.authorizedClientService.saveAuthorizedClient(any(), any())).thenReturn(Mono.empty()); + OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .refreshToken("refresh-1") + .build(); + when(this.exchange.getResponse().body(any())).thenReturn(Mono.just(response)); + Instant issuedAt = Instant.now().minus(Duration.ofDays(1)); + Instant accessTokenExpiresAt = issuedAt.plus(Duration.ofHours(1)); + Instant refreshTokenExpiresAt = Instant.now().plus(Duration.ofHours(1)); + + this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), + this.accessToken.getTokenValue(), + issuedAt, + accessTokenExpiresAt); + this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); + + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt, refreshTokenExpiresAt); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken, refreshToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange) + .block(); + + verify(this.authorizedClientService).saveAuthorizedClient(any(), any()); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(2); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://github.com/login/oauth/access_token"); + assertThat(request0.method()).isEqualTo(HttpMethod.POST); + assertThat(getBody(request0)).isEqualTo("grant_type=refresh_token&refresh_token=refresh-token"); + + ClientRequest request1 = requests.get(1); + assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-1"); + assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request1.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request1)).isEmpty(); + } + + @Test + public void filterWhenRefreshTokenNullThenShouldRefreshFalse() { + this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); + + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(1); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request0.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request0)).isEmpty(); + } + + @Test + public void filterWhenNotExpiredThenShouldRefreshFalse() { + this.function = new ServerOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientService); + + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt(), this.accessToken.getExpiresAt()); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken, refreshToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(1); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request0.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request0)).isEmpty(); + } + + private static String getBody(ClientRequest request) { + final List> messageWriters = new ArrayList<>(); + messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); + messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())); + messageWriters.add(new ResourceHttpMessageWriter()); + Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder(); + messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder)); + messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder)); + messageWriters.add(new FormHttpMessageWriter()); + messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes())); + messageWriters.add(new MultipartHttpMessageWriter(messageWriters)); + + BodyInserter.Context context = new BodyInserter.Context() { + @Override + public List> messageWriters() { + return messageWriters; + } + + @Override + public Optional serverRequest() { + return Optional.empty(); + } + + @Override + public Map hints() { + return new HashMap<>(); + } + }; + + MockClientHttpRequest body = new MockClientHttpRequest(HttpMethod.GET, "/"); + request.body().insert(body, context).block(); + return body.getBodyAsString().block(); + } +} 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 new file mode 100644 index 00000000000..378b8dcac6f --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunctionTests.java @@ -0,0 +1,492 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.reactive.function.client; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.core.codec.ByteBufferEncoder; +import org.springframework.core.codec.CharSequenceEncoder; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.codec.EncoderHttpMessageWriter; +import org.springframework.http.codec.FormHttpMessageWriter; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.codec.ResourceHttpMessageWriter; +import org.springframework.http.codec.ServerSentEventHttpMessageWriter; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.mock.http.client.reactive.MockClientHttpRequest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests { + @Mock + private OAuth2AuthorizedClientRepository authorizedClientRepository; + @Mock + private WebClient.RequestHeadersSpec spec; + @Captor + private ArgumentCaptor>> attrs; + + /** + * Used for get the attributes from defaultRequest. + */ + private Map result = new HashMap<>(); + + private ServletOAuth2AuthorizedClientExchangeFilterFunction function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(); + + private MockExchangeFunction exchange = new MockExchangeFunction(); + + private Authentication authentication; + + private ClientRegistration github = ClientRegistration.withRegistrationId("github") + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .scope("read:user") + .authorizationUri("https://github.com/login/oauth/authorize") + .tokenUri("https://github.com/login/oauth/access_token") + .userInfoUri("https://api.github.com/user") + .userNameAttributeName("id") + .clientName("GitHub") + .clientId("clientId") + .clientSecret("clientSecret") + .build(); + + private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + "token-0", + Instant.now(), + Instant.now().plus(Duration.ofDays(1))); + + @Before + public void setup() { + this.authentication = new TestingAuthenticationToken("test", "this"); + } + + @After + public void cleanup() { + SecurityContextHolder.clearContext(); + RequestContextHolder.resetRequestAttributes(); + } + + @Test + public void defaultRequestRequestResponseWhenNullRequestContextThenRequestAndResponseNull() { + Map attrs = getDefaultRequestAttributes(); + assertThat(getRequest(attrs)).isNull(); + assertThat(getResponse(attrs)).isNull(); + } + + @Test + public void defaultRequestRequestResponseWhenRequestContextThenRequestAndResponseSet() { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response)); + Map attrs = getDefaultRequestAttributes(); + assertThat(getRequest(attrs)).isEqualTo(request); + assertThat(getResponse(attrs)).isEqualTo(response); + } + + @Test + public void defaultRequestAuthenticationWhenSecurityContextEmptyThenAuthenticationNull() { + Map attrs = getDefaultRequestAttributes(); + assertThat(getAuthentication(attrs)).isNull(); + } + + @Test + public void defaultRequestAuthenticationWhenAuthenticationSetThenAuthenticationSet() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + SecurityContextHolder.getContext().setAuthentication(this.authentication); + Map attrs = getDefaultRequestAttributes(); + assertThat(getAuthentication(attrs)).isEqualTo(this.authentication); + verifyZeroInteractions(this.authorizedClientRepository); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenOAuth2AuthorizationClientAndClientIdThenNotOverride() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + oauth2AuthorizedClient(authorizedClient).accept(this.result); + Map attrs = getDefaultRequestAttributes(); + assertThat(getOAuth2AuthorizedClient(attrs)).isEqualTo(authorizedClient); + verifyZeroInteractions(this.authorizedClientRepository); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenAuthenticationNullAndClientRegistrationIdNullThenOAuth2AuthorizedClientNull() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + Map attrs = getDefaultRequestAttributes(); + assertThat(getOAuth2AuthorizedClient(attrs)).isNull(); + verifyZeroInteractions(this.authorizedClientRepository); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenAuthenticationWrongTypeAndClientRegistrationIdNullThenOAuth2AuthorizedClientNull() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + Map attrs = getDefaultRequestAttributes(); + assertThat(getOAuth2AuthorizedClient(attrs)).isNull(); + verifyZeroInteractions(this.authorizedClientRepository); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenRepositoryNullThenOAuth2AuthorizedClient() { + OAuth2User user = mock(OAuth2User.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(user, authorities, "id"); + authentication(token).accept(this.result); + + Map attrs = getDefaultRequestAttributes(); + + assertThat(getOAuth2AuthorizedClient(attrs)).isNull(); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenDefaultTrueAndAuthenticationAndClientRegistrationIdNullThenOAuth2AuthorizedClient() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + this.function.setDefaultOAuth2AuthorizedClient(true); + OAuth2User user = mock(OAuth2User.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(user, authorities, "id"); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(authorizedClient); + authentication(token).accept(this.result); + + Map attrs = getDefaultRequestAttributes(); + + assertThat(getOAuth2AuthorizedClient(attrs)).isEqualTo(authorizedClient); + verify(this.authorizedClientRepository).loadAuthorizedClient(eq(token.getAuthorizedClientRegistrationId()), any(), any()); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenDefaultFalseAndAuthenticationAndClientRegistrationIdNullThenOAuth2AuthorizedClient() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + OAuth2User user = mock(OAuth2User.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(user, authorities, "id"); + authentication(token).accept(this.result); + + Map attrs = getDefaultRequestAttributes(); + + assertThat(getOAuth2AuthorizedClient(attrs)).isNull(); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenAuthenticationAndClientRegistrationIdThenIdIsExplicit() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + OAuth2User user = mock(OAuth2User.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(user, authorities, "id"); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(authorizedClient); + authentication(token).accept(this.result); + clientRegistrationId("explicit").accept(this.result); + + Map attrs = getDefaultRequestAttributes(); + + assertThat(getOAuth2AuthorizedClient(attrs)).isEqualTo(authorizedClient); + verify(this.authorizedClientRepository).loadAuthorizedClient(eq("explicit"), any(), any()); + } + + @Test + public void defaultRequestOAuth2AuthorizedClientWhenAuthenticationNullAndClientRegistrationIdThenOAuth2AuthorizedClient() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + OAuth2User user = mock(OAuth2User.class); + List authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(authorizedClient); + clientRegistrationId("id").accept(this.result); + + Map attrs = getDefaultRequestAttributes(); + + assertThat(getOAuth2AuthorizedClient(attrs)).isEqualTo(authorizedClient); + verify(this.authorizedClientRepository).loadAuthorizedClient(eq("id"), any(), any()); + } + + private Map getDefaultRequestAttributes() { + this.function.defaultRequest().accept(this.spec); + verify(this.spec).attributes(this.attrs.capture()); + + this.attrs.getValue().accept(this.result); + + return this.result; + } + + @Test + public void filterWhenAuthorizedClientNullThenAuthorizationHeaderNull() { + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .build(); + + this.function.filter(request, this.exchange).block(); + + assertThat(this.exchange.getRequest().headers().getFirst(HttpHeaders.AUTHORIZATION)).isNull(); + } + + @Test + public void filterWhenAuthorizedClientThenAuthorizationHeader() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + assertThat(this.exchange.getRequest().headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue()); + } + + @Test + public void filterWhenExistingAuthorizationThenSingleAuthorizationHeader() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .header(HttpHeaders.AUTHORIZATION, "Existing") + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + HttpHeaders headers = this.exchange.getRequest().headers(); + assertThat(headers.get(HttpHeaders.AUTHORIZATION)).containsOnly("Bearer " + this.accessToken.getTokenValue()); + } + + @Test + public void filterWhenRefreshRequiredThenRefresh() { + OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .refreshToken("refresh-1") + .build(); + when(this.exchange.getResponse().body(any())).thenReturn(Mono.just(response)); + Instant issuedAt = Instant.now().minus(Duration.ofDays(1)); + Instant accessTokenExpiresAt = issuedAt.plus(Duration.ofHours(1)); + Instant refreshTokenExpiresAt = Instant.now().plus(Duration.ofHours(1)); + + this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), + this.accessToken.getTokenValue(), + issuedAt, + accessTokenExpiresAt); + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt, refreshTokenExpiresAt); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken, refreshToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .attributes(authentication(this.authentication)) + .build(); + + this.function.filter(request, this.exchange).block(); + + verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(this.authentication), any(), any()); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(2); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://github.com/login/oauth/access_token"); + assertThat(request0.method()).isEqualTo(HttpMethod.POST); + assertThat(getBody(request0)).isEqualTo("grant_type=refresh_token&refresh_token=refresh-token"); + + ClientRequest request1 = requests.get(1); + assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-1"); + assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request1.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request1)).isEmpty(); + } + + @Test + public void filterWhenRefreshRequiredAndEmptyReactiveSecurityContextThenSaved() { + OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .refreshToken("refresh-1") + .build(); + when(this.exchange.getResponse().body(any())).thenReturn(Mono.just(response)); + Instant issuedAt = Instant.now().minus(Duration.ofDays(1)); + Instant accessTokenExpiresAt = issuedAt.plus(Duration.ofHours(1)); + Instant refreshTokenExpiresAt = Instant.now().plus(Duration.ofHours(1)); + + this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(), + this.accessToken.getTokenValue(), + issuedAt, + accessTokenExpiresAt); + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt, refreshTokenExpiresAt); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken, refreshToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange) + .block(); + + verify(this.authorizedClientRepository).saveAuthorizedClient(any(), any(), any(), any()); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(2); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://github.com/login/oauth/access_token"); + assertThat(request0.method()).isEqualTo(HttpMethod.POST); + assertThat(getBody(request0)).isEqualTo("grant_type=refresh_token&refresh_token=refresh-token"); + + ClientRequest request1 = requests.get(1); + assertThat(request1.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-1"); + assertThat(request1.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request1.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request1)).isEmpty(); + } + + @Test + public void filterWhenRefreshTokenNullThenShouldRefreshFalse() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(1); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request0.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request0)).isEmpty(); + } + + @Test + public void filterWhenNotExpiredThenShouldRefreshFalse() { + this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.authorizedClientRepository); + + OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", this.accessToken.getIssuedAt(), this.accessToken.getExpiresAt()); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.github, + "principalName", this.accessToken, refreshToken); + ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .build(); + + this.function.filter(request, this.exchange).block(); + + List requests = this.exchange.getRequests(); + assertThat(requests).hasSize(1); + + ClientRequest request0 = requests.get(0); + assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0"); + assertThat(request0.url().toASCIIString()).isEqualTo("https://example.com"); + assertThat(request0.method()).isEqualTo(HttpMethod.GET); + assertThat(getBody(request0)).isEmpty(); + } + + private static String getBody(ClientRequest request) { + final List> messageWriters = new ArrayList<>(); + messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); + messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())); + messageWriters.add(new ResourceHttpMessageWriter()); + Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder(); + messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder)); + messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder)); + messageWriters.add(new FormHttpMessageWriter()); + messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes())); + messageWriters.add(new MultipartHttpMessageWriter(messageWriters)); + + BodyInserter.Context context = new BodyInserter.Context() { + @Override + public List> messageWriters() { + return messageWriters; + } + + @Override + public Optional serverRequest() { + return Optional.empty(); + } + + @Override + public Map hints() { + return new HashMap<>(); + } + }; + + MockClientHttpRequest body = new MockClientHttpRequest(HttpMethod.GET, "/"); + request.body().insert(body, context).block(); + return body.getBodyAsString().block(); + } + +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2ClientArgumentResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java similarity index 52% rename from oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2ClientArgumentResolverTests.java rename to oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java index aedcbaae57d..10ba47c4b89 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2ClientArgumentResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolverTests.java @@ -16,15 +16,6 @@ package org.springframework.security.oauth2.client.web.reactive.result.method.annotation; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.lang.reflect.Method; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,83 +28,49 @@ import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.ReflectionUtils; - import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.util.context.Context; +import java.lang.reflect.Method; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + /** * @author Rob Winch * @since 5.1 */ @RunWith(MockitoJUnitRunner.class) -public class OAuth2ClientArgumentResolverTests { - @Mock - private ReactiveClientRegistrationRepository clientRegistrationRepository; +public class OAuth2AuthorizedClientArgumentResolverTests { @Mock private ReactiveOAuth2AuthorizedClientService authorizedClientService; - private OAuth2ClientArgumentResolver argumentResolver; - private ClientRegistration clientRegistration; + private OAuth2AuthorizedClientArgumentResolver argumentResolver; private OAuth2AuthorizedClient authorizedClient; - private OAuth2AccessToken accessToken; private Authentication authentication = new TestingAuthenticationToken("test", "this"); @Before public void setUp() { - this.argumentResolver = new OAuth2ClientArgumentResolver( - this.clientRegistrationRepository, this.authorizedClientService); - this.clientRegistration = ClientRegistration.withRegistrationId("client1") - .clientId("client-id") - .clientSecret("secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate("{baseUrl}/client1") - .scope("scope1", "scope2") - .authorizationUri("https://provider.com/oauth2/auth") - .tokenUri("https://provider.com/oauth2/token") - .clientName("Client 1") - .build(); - when(this.clientRegistrationRepository.findByRegistrationId(anyString())).thenReturn(Mono.just(this.clientRegistration)); + this.argumentResolver = new OAuth2AuthorizedClientArgumentResolver(this.authorizedClientService); this.authorizedClient = mock(OAuth2AuthorizedClient.class); when(this.authorizedClientService.loadAuthorizedClient(anyString(), any())).thenReturn(Mono.just(this.authorizedClient)); - this.accessToken = mock(OAuth2AccessToken.class); - when(this.authorizedClient.getAccessToken()).thenReturn(this.accessToken); Hooks.onOperatorDebug(); } - @Test - public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientArgumentResolver(null, this.authorizedClientService)) - .isInstanceOf(IllegalArgumentException.class); - } - @Test public void constructorWhenOAuth2AuthorizedClientServiceIsNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientArgumentResolver(this.clientRegistrationRepository, null)) + assertThatThrownBy(() -> new OAuth2AuthorizedClientArgumentResolver(null)) .isInstanceOf(IllegalArgumentException.class); } - @Test - public void supportsParameterWhenParameterTypeOAuth2AccessTokenThenTrue() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessToken", OAuth2AccessToken.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isTrue(); - } - - @Test - public void supportsParameterWhenParameterTypeOAuth2AccessTokenWithoutAnnotationThenFalse() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessTokenWithoutAnnotation", OAuth2AccessToken.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); - } - @Test public void supportsParameterWhenParameterTypeOAuth2AuthorizedClientThenTrue() { MethodParameter methodParameter = this.getMethodParameter("paramTypeAuthorizedClient", OAuth2AuthorizedClient.class); @@ -126,18 +83,6 @@ public void supportsParameterWhenParameterTypeOAuth2AuthorizedClientWithoutAnnot assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); } - @Test - public void supportsParameterWhenParameterTypeClientRegistrationThenTrue() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistration", ClientRegistration.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isTrue(); - } - - @Test - public void supportsParameterWhenParameterTypeClientRegistrationWithoutAnnotationThenFalse() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistrationWithoutAnnotation", ClientRegistration.class); - assertThat(this.argumentResolver.supportsParameter(methodParameter)).isFalse(); - } - @Test public void supportsParameterWhenParameterTypeUnsupportedWithoutAnnotationThenFalse() { MethodParameter methodParameter = this.getMethodParameter("paramTypeUnsupportedWithoutAnnotation", String.class); @@ -146,10 +91,10 @@ public void supportsParameterWhenParameterTypeUnsupportedWithoutAnnotationThenFa @Test public void resolveArgumentWhenRegistrationIdEmptyAndNotOAuth2AuthenticationThenThrowIllegalArgumentException() { - MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AccessToken.class); + MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AuthorizedClient.class); assertThatThrownBy(() -> resolveArgument(methodParameter)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unable to resolve the Client Registration Identifier. It must be provided via @OAuth2Client(\"client1\") or @OAuth2Client(registrationId = \"client1\")."); + .hasMessage("Unable to resolve the Client Registration Identifier. It must be provided via @RegisteredOAuth2AuthorizedClient(\"client1\") or @RegisteredOAuth2AuthorizedClient(registrationId = \"client1\")."); } @Test @@ -157,25 +102,10 @@ public void resolveArgumentWhenRegistrationIdEmptyAndOAuth2AuthenticationThenRes this.authentication = mock(OAuth2AuthenticationToken.class); when(this.authentication.getName()).thenReturn("client1"); when(((OAuth2AuthenticationToken) this.authentication).getAuthorizedClientRegistrationId()).thenReturn("client1"); - MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AccessToken.class); + MethodParameter methodParameter = this.getMethodParameter("registrationIdEmpty", OAuth2AuthorizedClient.class); resolveArgument(methodParameter); } - @Test - public void resolveArgumentWhenClientRegistrationFoundThenResolves() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistration", ClientRegistration.class); - assertThat(resolveArgument(methodParameter)).isSameAs(this.clientRegistration); - } - - @Test - public void resolveArgumentWhenClientRegistrationNotFoundThenThrowIllegalArgumentException() { - when(this.clientRegistrationRepository.findByRegistrationId(anyString())).thenReturn(Mono.empty()); - MethodParameter methodParameter = this.getMethodParameter("paramTypeClientRegistration", ClientRegistration.class); - assertThatThrownBy(() -> resolveArgument(methodParameter)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unable to find ClientRegistration with registration identifier \"client1\"."); - } - @Test public void resolveArgumentWhenParameterTypeOAuth2AuthorizedClientAndCurrentAuthenticationNullThenThrowIllegalStateException() { this.authentication = null; @@ -201,26 +131,6 @@ public void resolveArgumentWhenOAuth2AuthorizedClientNotFoundThenThrowClientAuth .isInstanceOf(ClientAuthorizationRequiredException.class); } - @Test - public void resolveArgumentWhenOAuth2AccessTokenAndOAuth2AuthorizedClientFoundThenResolves() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessToken", OAuth2AccessToken.class); - assertThat(resolveArgument(methodParameter)).isSameAs(this.authorizedClient.getAccessToken()); - } - - @Test - public void resolveArgumentWhenOAuth2AccessTokenAndOAuth2AuthorizedClientNotFoundThenThrowClientAuthorizationRequiredException() { - when(this.authorizedClientService.loadAuthorizedClient(anyString(), any())).thenReturn(Mono.empty()); - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessToken", OAuth2AccessToken.class); - assertThatThrownBy(() -> resolveArgument(methodParameter)) - .isInstanceOf(ClientAuthorizationRequiredException.class); - } - - @Test - public void resolveArgumentWhenOAuth2AccessTokenAndAnnotationRegistrationIdSetThenResolves() { - MethodParameter methodParameter = this.getMethodParameter("paramTypeAccessTokenAnnotationRegistrationId", OAuth2AccessToken.class); - assertThat(resolveArgument(methodParameter)).isSameAs(this.authorizedClient.getAccessToken()); - } - private Object resolveArgument(MethodParameter methodParameter) { return this.argumentResolver.resolveArgument(methodParameter, null, null) .subscriberContext(this.authentication == null ? Context.empty() : ReactiveSecurityContextHolder.withAuthentication(this.authentication)) @@ -234,34 +144,19 @@ private MethodParameter getMethodParameter(String methodName, Class... paramT } static class TestController { - void paramTypeAccessToken(@OAuth2Client("client1") OAuth2AccessToken accessToken) { - } - - void paramTypeAccessTokenWithoutAnnotation(OAuth2AccessToken accessToken) { - } - - void paramTypeAuthorizedClient(@OAuth2Client("client1") OAuth2AuthorizedClient authorizedClient) { + void paramTypeAuthorizedClient(@RegisteredOAuth2AuthorizedClient("client1") OAuth2AuthorizedClient authorizedClient) { } void paramTypeAuthorizedClientWithoutAnnotation(OAuth2AuthorizedClient authorizedClient) { } - void paramTypeClientRegistration(@OAuth2Client("client1") ClientRegistration clientRegistration) { - } - - void paramTypeClientRegistrationWithoutAnnotation(ClientRegistration clientRegistration) { - } - - void paramTypeUnsupported(@OAuth2Client("client1") String param) { + void paramTypeUnsupported(@RegisteredOAuth2AuthorizedClient("client1") String param) { } void paramTypeUnsupportedWithoutAnnotation(String param) { } - void registrationIdEmpty(@OAuth2Client OAuth2AccessToken accessToken) { - } - - void paramTypeAccessTokenAnnotationRegistrationId(@OAuth2Client(registrationId = "client1") OAuth2AccessToken accessToken) { + void registrationIdEmpty(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) { } } } diff --git a/oauth2/oauth2-core/spring-security-oauth2-core.gradle b/oauth2/oauth2-core/spring-security-oauth2-core.gradle index 0a477bf7c32..bc66851194d 100644 --- a/oauth2/oauth2-core/spring-security-oauth2-core.gradle +++ b/oauth2/oauth2-core/spring-security-oauth2-core.gradle @@ -4,5 +4,9 @@ dependencies { compile project(':spring-security-core') compile springCoreDependency + optional 'com.fasterxml.jackson.core:jackson-databind' + optional 'com.nimbusds:oauth2-oidc-sdk' + optional 'org.springframework:spring-webflux' + testCompile powerMock2Dependencies } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthenticationMethod.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthenticationMethod.java new file mode 100644 index 00000000000..e74ee7ef98c --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthenticationMethod.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.core; + +import java.io.Serializable; + +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.util.Assert; + +/** + * The authentication method used when sending bearer access tokens in resource requests to resource servers. + * + * @author MyeongHyeon Lee + * @since 5.1 + * @see Section 2 Authenticated Requests + */ +public final class AuthenticationMethod implements Serializable { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + public static final AuthenticationMethod HEADER = new AuthenticationMethod("header"); + public static final AuthenticationMethod FORM = new AuthenticationMethod("form"); + public static final AuthenticationMethod QUERY = new AuthenticationMethod("query"); + private final String value; + + /** + * Constructs an {@code AuthenticationMethod} using the provided value. + * + * @param value the value of the authentication method type + */ + public AuthenticationMethod(String value) { + Assert.hasText(value, "value cannot be empty"); + this.value = value; + } + + /** + * Returns the value of the authentication method type. + * + * @return the value of the authentication method type + */ + public String getValue() { + return this.value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + AuthenticationMethod that = (AuthenticationMethod) obj; + return this.getValue().equals(that.getValue()); + } + + @Override + public int hashCode() { + return this.getValue().hashCode(); + } +} 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 61c596162eb..1a0af578062 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-2017 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. @@ -37,6 +37,8 @@ public final class AuthorizationGrantType implements Serializable { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; public static final AuthorizationGrantType AUTHORIZATION_CODE = new AuthorizationGrantType("authorization_code"); public static final AuthorizationGrantType IMPLICIT = new AuthorizationGrantType("implicit"); + public static final AuthorizationGrantType REFRESH_TOKEN = new AuthorizationGrantType("refresh_token"); + public static final AuthorizationGrantType CLIENT_CREDENTIALS = new AuthorizationGrantType("client_credentials"); 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 f9bc43b2911..e4505a4a907 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-2017 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. @@ -53,13 +53,17 @@ default Boolean containsClaim(String claim) { } /** - * Returns the claim value as a {@code String} or {@code null} if it does not exist. + * Returns the claim value as a {@code String} or {@code null} if it does not exist or is equal to {@code null}. * * @param claim the name of the claim - * @return the claim value or {@code null} if it does not exist + * @return the claim value or {@code null} if it does not exist or is equal to {@code null} */ default String getClaimAsString(String claim) { - return (this.containsClaim(claim) ? this.getClaims().get(claim).toString() : null); + if (!this.containsClaim(claim)) { + return null; + } + Object claimValue = this.getClaims().get(claim); + return (claimValue != null ? claimValue.toString() : null); } /** diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidator.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidator.java new file mode 100644 index 00000000000..dd6ae3d8a52 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidator.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.util.ArrayList; +import java.util.Collection; + +import org.springframework.util.Assert; + +/** + * A composite validator + * + * @param the type of {@link AbstractOAuth2Token} this validator validates + * + * @author Josh Cummings + * @since 5.1 + */ +public final class DelegatingOAuth2TokenValidator + implements OAuth2TokenValidator { + + private final Collection> tokenValidators; + + public DelegatingOAuth2TokenValidator(Collection> tokenValidators) { + Assert.notNull(tokenValidators, "tokenValidators cannot be null"); + + this.tokenValidators = new ArrayList<>(tokenValidators); + } + + /** + * {@inheritDoc} + */ + @Override + public OAuth2TokenValidatorResult validate(T token) { + Collection errors = new ArrayList<>(); + + for ( OAuth2TokenValidator validator : this.tokenValidators) { + errors.addAll(validator.validate(token).getErrors()); + } + + return OAuth2TokenValidatorResult.failure(errors); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java index 20a3014e911..8e18df55a12 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -15,8 +15,10 @@ */ package org.springframework.security.oauth2.core; +import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.util.Assert; +import java.io.Serializable; import java.time.Instant; import java.util.Collections; import java.util.Set; @@ -90,7 +92,8 @@ public Set getScopes() { * * @see Section 7.1 Access Token Types */ - public static final class TokenType { + public static final class TokenType implements Serializable { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; public static final TokenType BEARER = new TokenType("Bearer"); private final String value; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticationException.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticationException.java index ab0b8c050c4..3229b04b002 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticationException.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -40,6 +40,15 @@ public class OAuth2AuthenticationException extends AuthenticationException { private OAuth2Error error; + /** + * Constructs an {@code OAuth2AuthenticationException} using the provided parameters. + * + * @param error the {@link OAuth2Error OAuth 2.0 Error} + */ + public OAuth2AuthenticationException(OAuth2Error error) { + this(error, error.getDescription()); + } + /** * Constructs an {@code OAuth2AuthenticationException} using the provided parameters. * diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java new file mode 100644 index 00000000000..7070c810248 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 an OAuth 2.0 Refresh Token. + * + *

      + * A refresh token is a credential that represents an authorization + * granted by the resource owner to the client. + * It is used by the client to obtain a new access token when the current access token + * becomes invalid or expires, or to obtain additional access tokens with identical or narrower scope. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AccessToken + * @see Section 1.5 Refresh Token + */ +public class OAuth2RefreshToken extends AbstractOAuth2Token { + + /** + * Constructs an {@code OAuth2RefreshToken} using the provided parameters. + * + * @param tokenValue the token value + * @param issuedAt the time at which the token was issued + * @param expiresAt the expiration time on or after which the token MUST NOT be accepted + */ + public OAuth2RefreshToken(String tokenValue, Instant issuedAt, Instant expiresAt) { + super(tokenValue, issuedAt, expiresAt); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidator.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidator.java new file mode 100644 index 00000000000..769f351a7ba --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidator.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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; + +/** + * Implementations of this interface are responsible for "verifying" + * the validity and/or constraints of the attributes contained in an OAuth 2.0 Token. + * + * @author Joe Grandja + * @author Josh Cummings + * @since 5.1 + */ +public interface OAuth2TokenValidator { + + /** + * Verify the validity and/or constraints of the provided OAuth 2.0 Token. + * + * @param token an OAuth 2.0 token + * @return OAuth2TokenValidationResult the success or failure detail of the validation + */ + OAuth2TokenValidatorResult validate(T token); +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResult.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResult.java new file mode 100644 index 00000000000..247fbe391be --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResult.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.springframework.util.Assert; + +/** + * A result emitted from an {@link OAuth2TokenValidator} validation attempt + * + * @author Josh Cummings + * @since 5.1 + */ +public final class OAuth2TokenValidatorResult { + static final OAuth2TokenValidatorResult NO_ERRORS = new OAuth2TokenValidatorResult(Collections.emptyList()); + + private final Collection errors; + + private OAuth2TokenValidatorResult(Collection errors) { + Assert.notNull(errors, "errors cannot be null"); + this.errors = new ArrayList<>(errors); + } + + /** + * Say whether this result indicates success + * + * @return whether this result has errors + */ + public boolean hasErrors() { + return !this.errors.isEmpty(); + } + + /** + * Return error details regarding the validation attempt + * + * @return the collection of results in this result, if any; returns an empty list otherwise + */ + public Collection getErrors() { + return this.errors; + } + + /** + * Construct a successful {@link OAuth2TokenValidatorResult} + * + * @return an {@link OAuth2TokenValidatorResult} with no errors + */ + public static OAuth2TokenValidatorResult success() { + return NO_ERRORS; + } + + /** + * Construct a failure {@link OAuth2TokenValidatorResult} with the provided detail + * + * @param errors the list of errors + * @return an {@link OAuth2TokenValidatorResult} with the errors specified + */ + public static OAuth2TokenValidatorResult failure(OAuth2Error... errors) { + return failure(Arrays.asList(errors)); + } + + /** + * Construct a failure {@link OAuth2TokenValidatorResult} with the provided detail + * + * @param errors the list of errors + * @return an {@link OAuth2TokenValidatorResult} with the errors specified + */ + public static OAuth2TokenValidatorResult failure(Collection errors) { + if (errors.isEmpty()) { + return NO_ERRORS; + } + + return new OAuth2TokenValidatorResult(errors); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java index 3630ee449c1..38cdbbbc6a5 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -15,8 +15,11 @@ */ package org.springframework.security.oauth2.core.endpoint; +import org.springframework.lang.Nullable; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import java.time.Instant; import java.util.Collections; @@ -29,10 +32,12 @@ * @author Joe Grandja * @since 5.0 * @see OAuth2AccessToken + * @see OAuth2RefreshToken * @see Section 5.1 Access Token Response */ public final class OAuth2AccessTokenResponse { private OAuth2AccessToken accessToken; + private OAuth2RefreshToken refreshToken; private Map additionalParameters; private OAuth2AccessTokenResponse() { @@ -47,6 +52,16 @@ public OAuth2AccessToken getAccessToken() { return this.accessToken; } + /** + * Returns the {@link OAuth2RefreshToken Refresh Token}. + * + * @since 5.1 + * @return the {@link OAuth2RefreshToken} + */ + public @Nullable OAuth2RefreshToken getRefreshToken() { + return this.refreshToken; + } + /** * Returns the additional parameters returned in the response. * @@ -66,6 +81,15 @@ public static Builder withToken(String tokenValue) { return new Builder(tokenValue); } + /** + * Returns a new {@link Builder}, initialized with the provided response + * @param response the response to intialize the builder with + * @return the {@link Builder} + */ + public static Builder withResponse(OAuth2AccessTokenResponse response) { + return new Builder(response); + } + /** * A builder for {@link OAuth2AccessTokenResponse}. */ @@ -74,8 +98,24 @@ public static class Builder { private OAuth2AccessToken.TokenType tokenType; private long expiresIn; private Set scopes; + private String refreshToken; private Map additionalParameters; + private Instant issuedAt; + private Instant expiresAt; + + private Builder(OAuth2AccessTokenResponse response) { + OAuth2AccessToken accessToken = response.getAccessToken(); + this.tokenValue = accessToken.getTokenValue(); + this.tokenType = accessToken.getTokenType(); + this.expiresAt = accessToken.getExpiresAt(); + this.issuedAt = accessToken.getIssuedAt(); + this.scopes = accessToken.getScopes(); + this.refreshToken = response.getRefreshToken() == null ? + null : response.getRefreshToken().getTokenValue(); + this.additionalParameters = response.getAdditionalParameters(); + } + private Builder(String tokenValue) { this.tokenValue = tokenValue; } @@ -113,6 +153,17 @@ public Builder scopes(Set scopes) { return this; } + /** + * Sets the refresh token associated to the access token. + * + * @param refreshToken the refresh token associated to the access token. + * @return the {@link Builder} + */ + public Builder refreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + /** * Sets the additional parameters returned in the response. * @@ -130,21 +181,47 @@ public Builder additionalParameters(Map additionalParameters) { * @return a {@link OAuth2AccessTokenResponse} */ public OAuth2AccessTokenResponse build() { - Instant issuedAt = Instant.now(); + Instant issuedAt = getIssuedAt(); - // expires_in is RECOMMENDED, as per spec https://tools.ietf.org/html/rfc6749#section-5.1 - // Therefore, expires_in may not be returned in the Access Token response which would result in the default value of 0. - // For these instances, default the expiresAt to +1 second from issuedAt time. - Instant expiresAt = this.expiresIn > 0 ? - issuedAt.plusSeconds(this.expiresIn) : - issuedAt.plusSeconds(1); + Instant expiresAt = getExpiresAt(); OAuth2AccessTokenResponse accessTokenResponse = new OAuth2AccessTokenResponse(); accessTokenResponse.accessToken = new OAuth2AccessToken( this.tokenType, this.tokenValue, issuedAt, expiresAt, this.scopes); + if (StringUtils.hasText(this.refreshToken)) { + // The Access Token response does not return an expires_in for the Refresh Token, + // therefore, we'll default to +1 second from issuedAt time. + // NOTE: + // The expiry or invalidity of a Refresh Token can only be determined by performing + // the refresh_token grant and if it fails than likely it has expired or has been invalidated. + accessTokenResponse.refreshToken = new OAuth2RefreshToken(this.refreshToken, issuedAt, issuedAt.plusSeconds(1)); + } accessTokenResponse.additionalParameters = Collections.unmodifiableMap( CollectionUtils.isEmpty(this.additionalParameters) ? Collections.emptyMap() : this.additionalParameters); return accessTokenResponse; } + + private Instant getIssuedAt() { + if (this.issuedAt == null) { + this.issuedAt = Instant.now(); + } + return this.issuedAt; + } + + /** + * expires_in is RECOMMENDED, as per spec https://tools.ietf.org/html/rfc6749#section-5.1 + * Therefore, expires_in may not be returned in the Access Token response which would result in the default value of 0. + * For these instances, default the expiresAt to +1 second from issuedAt time. + * @return + */ + private Instant getExpiresAt() { + if (this.expiresAt == null) { + Instant issuedAt = getIssuedAt(); + this.expiresAt = this.expiresIn > 0 ? + issuedAt.plusSeconds(this.expiresIn) : + issuedAt.plusSeconds(1); + } + return this.expiresAt; + } } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java index 78eaac5598c..3db5f3541d7 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -19,14 +19,18 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.StringJoiner; import java.util.stream.Collectors; /** @@ -50,6 +54,7 @@ public final class OAuth2AuthorizationRequest implements Serializable { private Set scopes; private String state; private Map additionalParameters; + private String authorizationRequestUri; private OAuth2AuthorizationRequest() { } @@ -126,6 +131,20 @@ public Map getAdditionalParameters() { return this.additionalParameters; } + /** + * Returns the {@code URI} string representation of the OAuth 2.0 Authorization Request. + * + *

      + * NOTE: The {@code URI} string is encoded in the + * {@code application/x-www-form-urlencoded} MIME format. + * + * @since 5.1 + * @return the {@code URI} string representation of the OAuth 2.0 Authorization Request + */ + public String getAuthorizationRequestUri() { + return this.authorizationRequestUri; + } + /** * Returns a new {@link Builder}, initialized with the authorization code grant type. * @@ -144,6 +163,26 @@ public static Builder implicit() { return new Builder(AuthorizationGrantType.IMPLICIT); } + /** + * Returns a new {@link Builder}, initialized with the values + * from the provided {@code authorizationRequest}. + * + * @since 5.1 + * @param authorizationRequest the authorization request used for initializing the {@link Builder} + * @return the {@link Builder} + */ + public static Builder from(OAuth2AuthorizationRequest authorizationRequest) { + Assert.notNull(authorizationRequest, "authorizationRequest cannot be null"); + + return new Builder(authorizationRequest.getGrantType()) + .authorizationUri(authorizationRequest.getAuthorizationUri()) + .clientId(authorizationRequest.getClientId()) + .redirectUri(authorizationRequest.getRedirectUri()) + .scopes(authorizationRequest.getScopes()) + .state(authorizationRequest.getState()) + .additionalParameters(authorizationRequest.getAdditionalParameters()); + } + /** * A builder for {@link OAuth2AuthorizationRequest}. */ @@ -156,6 +195,7 @@ public static class Builder { private Set scopes; private String state; private Map additionalParameters; + private String authorizationRequestUri; private Builder(AuthorizationGrantType authorizationGrantType) { Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null"); @@ -247,6 +287,22 @@ public Builder additionalParameters(Map additionalParameters) { return this; } + /** + * Sets the {@code URI} string representation of the OAuth 2.0 Authorization Request. + * + *

      + * NOTE: The {@code URI} string is required to be encoded in the + * {@code application/x-www-form-urlencoded} MIME format. + * + * @since 5.1 + * @param authorizationRequestUri the {@code URI} string representation of the OAuth 2.0 Authorization Request + * @return the {@link Builder} + */ + public Builder authorizationRequestUri(String authorizationRequestUri) { + this.authorizationRequestUri = authorizationRequestUri; + return this; + } + /** * Builds a new {@link OAuth2AuthorizationRequest}. * @@ -272,7 +328,42 @@ public OAuth2AuthorizationRequest build() { authorizationRequest.additionalParameters = Collections.unmodifiableMap( CollectionUtils.isEmpty(this.additionalParameters) ? Collections.emptyMap() : new LinkedHashMap<>(this.additionalParameters)); + authorizationRequest.authorizationRequestUri = + StringUtils.hasText(this.authorizationRequestUri) ? + this.authorizationRequestUri : this.buildAuthorizationRequestUri(); + return authorizationRequest; } + + private String buildAuthorizationRequestUri() { + Map parameters = new LinkedHashMap<>(); + parameters.put(OAuth2ParameterNames.RESPONSE_TYPE, this.responseType.getValue()); + parameters.put(OAuth2ParameterNames.CLIENT_ID, this.clientId); + if (!CollectionUtils.isEmpty(this.scopes)) { + parameters.put(OAuth2ParameterNames.SCOPE, + StringUtils.collectionToDelimitedString(this.scopes, " ")); + } + if (this.state != null) { + parameters.put(OAuth2ParameterNames.STATE, this.state); + } + if (this.redirectUri != null) { + parameters.put(OAuth2ParameterNames.REDIRECT_URI, this.redirectUri); + } + if (!CollectionUtils.isEmpty(this.additionalParameters)) { + this.additionalParameters.entrySet().stream() + .filter(e -> !e.getKey().equals(OAuth2ParameterNames.REGISTRATION_ID)) + .forEach(e -> parameters.put(e.getKey(), e.getValue().toString())); + } + + try { + StringJoiner queryParams = new StringJoiner("&"); + for (String paramName : parameters.keySet()) { + queryParams.add(paramName + "=" + URLEncoder.encode(parameters.get(paramName), "UTF-8")); + } + return this.authorizationUri + "?" + queryParams.toString(); + } catch (UnsupportedEncodingException ex) { + throw new IllegalArgumentException("Unable to build authorization request uri: " + ex.getMessage(), ex); + } + } } } 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 eed944b0169..c1061e7c2e3 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-2017 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. @@ -25,6 +25,11 @@ */ public interface OAuth2ParameterNames { + /** + * {@code grant_type} - used in Access Token Request. + */ + String GRANT_TYPE = "grant_type"; + /** * {@code response_type} - used in Authorization Request. */ @@ -35,6 +40,11 @@ public interface OAuth2ParameterNames { */ String CLIENT_ID = "client_id"; + /** + * {@code client_secret} - used in Access Token Request. + */ + String CLIENT_SECRET = "client_secret"; + /** * {@code redirect_uri} - used in Authorization Request and Access Token Request. */ @@ -55,6 +65,26 @@ public interface OAuth2ParameterNames { */ String CODE = "code"; + /** + * {@code access_token} - used in Authorization Response and Access Token Response. + */ + String ACCESS_TOKEN = "access_token"; + + /** + * {@code token_type} - used in Authorization Response and Access Token Response. + */ + String TOKEN_TYPE = "token_type"; + + /** + * {@code expires_in} - used in Authorization Response and Access Token Response. + */ + String EXPIRES_IN = "expires_in"; + + /** + * {@code refresh_token} - used in Access Token Request and Access Token Response. + */ + String REFRESH_TOKEN = "refresh_token"; + /** * {@code error} - used in Authorization Response and Access Token Response. */ diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2AccessTokenResponseBodyExtractor.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2AccessTokenResponseBodyExtractor.java new file mode 100644 index 00000000000..188f2bd0d54 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2AccessTokenResponseBodyExtractor.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.reactive.function; + +import com.nimbusds.oauth2.sdk.AccessTokenResponse; +import com.nimbusds.oauth2.sdk.ErrorObject; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.TokenErrorResponse; +import com.nimbusds.oauth2.sdk.TokenResponse; +import com.nimbusds.oauth2.sdk.token.AccessToken; +import net.minidev.json.JSONObject; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.web.reactive.function.BodyExtractor; +import org.springframework.web.reactive.function.BodyExtractors; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * Provides a way to create an {@link OAuth2AccessTokenResponse} from a {@link ReactiveHttpInputMessage} + * @author Rob Winch + * @since 5.1 + */ +class OAuth2AccessTokenResponseBodyExtractor + implements BodyExtractor, ReactiveHttpInputMessage> { + + private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response"; + + OAuth2AccessTokenResponseBodyExtractor() {} + + @Override + public Mono extract(ReactiveHttpInputMessage inputMessage, + Context context) { + ParameterizedTypeReference> type = new ParameterizedTypeReference>() {}; + BodyExtractor>, ReactiveHttpInputMessage> delegate = BodyExtractors.toMono(type); + return delegate.extract(inputMessage, context) + .map(json -> parse(json)) + .flatMap(OAuth2AccessTokenResponseBodyExtractor::oauth2AccessTokenResponse) + .map(OAuth2AccessTokenResponseBodyExtractor::oauth2AccessTokenResponse); + } + + private static TokenResponse parse(Map json) { + try { + return TokenResponse.parse(new JSONObject(json)); + } + catch (ParseException pe) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, + "An error occurred parsing the Access Token response: " + pe.getMessage(), null); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), pe); + } + } + + private static Mono oauth2AccessTokenResponse(TokenResponse tokenResponse) { + if (tokenResponse.indicatesSuccess()) { + return Mono.just(tokenResponse) + .cast(AccessTokenResponse.class); + } + TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse; + ErrorObject errorObject = tokenErrorResponse.getErrorObject(); + OAuth2Error oauth2Error; + if (errorObject == null) { + oauth2Error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR); + } else { + oauth2Error = new OAuth2Error( + errorObject.getCode() != null ? errorObject.getCode() : OAuth2ErrorCodes.SERVER_ERROR, + errorObject.getDescription(), + errorObject.getURI() != null ? errorObject.getURI().toString() : null); + } + return Mono.error(new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString())); + } + + private static OAuth2AccessTokenResponse oauth2AccessTokenResponse(AccessTokenResponse accessTokenResponse) { + AccessToken accessToken = accessTokenResponse.getTokens().getAccessToken(); + OAuth2AccessToken.TokenType accessTokenType = null; + if (OAuth2AccessToken.TokenType.BEARER.getValue() + .equalsIgnoreCase(accessToken.getType().getValue())) { + accessTokenType = OAuth2AccessToken.TokenType.BEARER; + } + long expiresIn = accessToken.getLifetime(); + + Set scopes = accessToken.getScope() == null ? + Collections.emptySet() : new LinkedHashSet<>(accessToken.getScope().toStringList()); + + String refreshToken = null; + if (accessTokenResponse.getTokens().getRefreshToken() != null) { + refreshToken = accessTokenResponse.getTokens().getRefreshToken().getValue(); + } + + Map additionalParameters = new LinkedHashMap<>(accessTokenResponse.getCustomParameters()); + + return OAuth2AccessTokenResponse.withToken(accessToken.getValue()).tokenType(accessTokenType).expiresIn(expiresIn).scopes(scopes) + .refreshToken(refreshToken).additionalParameters(additionalParameters).build(); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractors.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractors.java new file mode 100644 index 00000000000..fbffe082eb7 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractors.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.reactive.function; + +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.web.reactive.function.BodyExtractor; +import reactor.core.publisher.Mono; + +/** + * Static factory methods for OAuth2 {@link BodyExtractor} implementations. + * @author Rob Winch + * @since 5.1 + */ +public abstract class OAuth2BodyExtractors { + + /** + * Extractor to decode an {@link OAuth2AccessTokenResponse} + * @return a BodyExtractor for {@link OAuth2AccessTokenResponse} + */ + public static BodyExtractor, ReactiveHttpInputMessage> oauth2AccessTokenResponse() { + return new OAuth2AccessTokenResponseBodyExtractor(); + } + + private OAuth2BodyExtractors() {} +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthenticationMethodTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthenticationMethodTests.java new file mode 100644 index 00000000000..4c541cf1506 --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthenticationMethodTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 static org.assertj.core.api.Assertions.*; + +import org.junit.Test; + +/** + * Tests for {@link AuthenticationMethod}. + * + * @author MyeongHyeon Lee + */ +public class AuthenticationMethodTests { + + @Test + public void constructorWhenValueIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new AuthenticationMethod(null)).hasMessage("value cannot be empty"); + } + + @Test + public void getValueWhenHeaderAuthenticationTypeThenReturnHeader() { + assertThat(AuthenticationMethod.HEADER.getValue()).isEqualTo("header"); + } + + @Test + public void getValueWhenFormAuthenticationTypeThenReturnForm() { + assertThat(AuthenticationMethod.FORM.getValue()).isEqualTo("form"); + } + + @Test + public void getValueWhenFormAuthenticationTypeThenReturnQuery() { + assertThat(AuthenticationMethod.QUERY.getValue()).isEqualTo("query"); + } +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java index dea90e92b1e..2d94930c48c 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/AuthorizationGrantTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -40,4 +40,9 @@ public void getValueWhenAuthorizationCodeGrantTypeThenReturnAuthorizationCode() public void getValueWhenImplicitGrantTypeThenReturnImplicit() { assertThat(AuthorizationGrantType.IMPLICIT.getValue()).isEqualTo("implicit"); } + + @Test + public void getValueWhenRefreshTokenGrantTypeThenReturnRefreshToken() { + assertThat(AuthorizationGrantType.REFRESH_TOKEN.getValue()).isEqualTo("refresh_token"); + } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClaimAccessorTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClaimAccessorTests.java index 4a7f0efe02d..dc89d1dbe63 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClaimAccessorTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ClaimAccessorTests.java @@ -92,4 +92,13 @@ public void getClaimAsInstantWhenDoubleTypeSecondsThenReturnInstant() { assertThat(this.claimAccessor.getClaimAsInstant(claimName)).isBetween( expectedClaimValue.minusSeconds(1), expectedClaimValue.plusSeconds(1)); } + + // gh-5608 + @Test + public void getClaimAsStringWhenValueIsNullThenReturnNull() { + String claimName = "claim-with-null-value"; + this.claims.put(claimName, null); + + assertThat(this.claimAccessor.getClaimAsString(claimName)).isNull(); + } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidatorTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidatorTests.java new file mode 100644 index 00000000000..3203b66932e --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/DelegatingOAuth2TokenValidatorTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.util.Arrays; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for verifying {@link DelegatingOAuth2TokenValidator} + * + * @author Josh Cummings + */ +public class DelegatingOAuth2TokenValidatorTests { + private static final OAuth2Error DETAIL = new OAuth2Error( + "error", "description", "uri"); + + @Test + public void validateWhenNoValidatorsConfiguredThenReturnsSuccessfulResult() { + DelegatingOAuth2TokenValidator tokenValidator = + new DelegatingOAuth2TokenValidator<>(Collections.emptyList()); + AbstractOAuth2Token token = mock(AbstractOAuth2Token.class); + + assertThat(tokenValidator.validate(token).hasErrors()).isFalse(); + } + + @Test + public void validateWhenAnyValidatorFailsThenReturnsFailureResultContainingDetailFromFailingValidator() { + OAuth2TokenValidator success = mock(OAuth2TokenValidator.class); + OAuth2TokenValidator failure = mock(OAuth2TokenValidator.class); + + when(success.validate(any(AbstractOAuth2Token.class))) + .thenReturn(OAuth2TokenValidatorResult.success()); + when(failure.validate(any(AbstractOAuth2Token.class))) + .thenReturn(OAuth2TokenValidatorResult.failure(DETAIL)); + + DelegatingOAuth2TokenValidator tokenValidator = + new DelegatingOAuth2TokenValidator<>(Arrays.asList(success, failure)); + AbstractOAuth2Token token = mock(AbstractOAuth2Token.class); + + OAuth2TokenValidatorResult result = + tokenValidator.validate(token); + + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors()).containsExactly(DETAIL); + } + + @Test + public void validateWhenMultipleValidatorsFailThenReturnsFailureResultContainingAllDetails() { + OAuth2TokenValidator firstFailure = mock(OAuth2TokenValidator.class); + OAuth2TokenValidator secondFailure = mock(OAuth2TokenValidator.class); + + OAuth2Error otherDetail = new OAuth2Error("another-error"); + + when(firstFailure.validate(any(AbstractOAuth2Token.class))) + .thenReturn(OAuth2TokenValidatorResult.failure(DETAIL)); + when(secondFailure.validate(any(AbstractOAuth2Token.class))) + .thenReturn(OAuth2TokenValidatorResult.failure(otherDetail)); + + DelegatingOAuth2TokenValidator tokenValidator = + new DelegatingOAuth2TokenValidator<>(Arrays.asList(firstFailure, secondFailure)); + AbstractOAuth2Token token = mock(AbstractOAuth2Token.class); + + OAuth2TokenValidatorResult result = + tokenValidator.validate(token); + + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors()).containsExactly(DETAIL, otherDetail); + } + + @Test + public void validateWhenAllValidatorsSucceedThenReturnsSuccessfulResult() { + OAuth2TokenValidator firstSuccess = mock(OAuth2TokenValidator.class); + OAuth2TokenValidator secondSuccess = mock(OAuth2TokenValidator.class); + + when(firstSuccess.validate(any(AbstractOAuth2Token.class))) + .thenReturn(OAuth2TokenValidatorResult.success()); + when(secondSuccess.validate(any(AbstractOAuth2Token.class))) + .thenReturn(OAuth2TokenValidatorResult.success()); + + DelegatingOAuth2TokenValidator tokenValidator = + new DelegatingOAuth2TokenValidator<>(Arrays.asList(firstSuccess, secondSuccess)); + AbstractOAuth2Token token = mock(AbstractOAuth2Token.class); + + OAuth2TokenValidatorResult result = + tokenValidator.validate(token); + + assertThat(result.hasErrors()).isFalse(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void constructorWhenInvokedWithNullValidatorListThenThrowsIllegalArgumentException() { + assertThatCode(() -> new DelegatingOAuth2TokenValidator<>(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java index 7745856fde6..4309cbb6450 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2AccessTokenTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.core; import org.junit.Test; +import org.springframework.util.SerializationUtils; import java.time.Instant; import java.util.Arrays; @@ -72,4 +73,20 @@ public void constructorWhenAllParametersProvidedAndValidThenCreated() { assertThat(accessToken.getExpiresAt()).isEqualTo(EXPIRES_AT); assertThat(accessToken.getScopes()).isEqualTo(SCOPES); } + + // gh-5492 + @Test + public void constructorWhenCreatedThenIsSerializableAndDeserializable() { + OAuth2AccessToken accessToken = new OAuth2AccessToken( + TOKEN_TYPE, TOKEN_VALUE, ISSUED_AT, EXPIRES_AT, SCOPES); + byte[] serialized = SerializationUtils.serialize(accessToken); + accessToken = (OAuth2AccessToken) SerializationUtils.deserialize(serialized); + + assertThat(serialized).isNotNull(); + assertThat(accessToken.getTokenType()).isEqualTo(TOKEN_TYPE); + assertThat(accessToken.getTokenValue()).isEqualTo(TOKEN_VALUE); + assertThat(accessToken.getIssuedAt()).isEqualTo(ISSUED_AT); + assertThat(accessToken.getExpiresAt()).isEqualTo(EXPIRES_AT); + assertThat(accessToken.getScopes()).isEqualTo(SCOPES); + } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResultTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResultTests.java new file mode 100644 index 00000000000..dd43a31da32 --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/OAuth2TokenValidatorResultTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 org.junit.Test; + +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for verifying {@link OAuth2TokenValidatorResult} + * + * @author Josh Cummings + */ +public class OAuth2TokenValidatorResultTests { + private static final OAuth2Error DETAIL = new OAuth2Error( + "error", "description", "uri"); + + @Test + public void successWhenInvokedThenReturnsSuccessfulResult() { + OAuth2TokenValidatorResult success = OAuth2TokenValidatorResult.success(); + assertThat(success.hasErrors()).isFalse(); + } + + @Test + public void failureWhenInvokedWithDetailReturnsFailureResultIncludingDetail() { + OAuth2TokenValidatorResult failure = OAuth2TokenValidatorResult.failure(DETAIL); + + assertThat(failure.hasErrors()).isTrue(); + assertThat(failure.getErrors()).containsExactly(DETAIL); + } + + @Test + public void failureWhenInvokedWithMultipleDetailsReturnsFailureResultIncludingAll() { + OAuth2TokenValidatorResult failure = OAuth2TokenValidatorResult.failure(DETAIL, DETAIL); + + assertThat(failure.hasErrors()).isTrue(); + assertThat(failure.getErrors()).containsExactly(DETAIL, DETAIL); + } +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java index 2346347aad2..fe2b9483360 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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,6 +35,7 @@ */ public class OAuth2AccessTokenResponseTests { private static final String TOKEN_VALUE = "access-token"; + private static final String REFRESH_TOKEN_VALUE = "refresh-token"; private static final long EXPIRES_IN = Instant.now().plusSeconds(5).toEpochMilli(); @Test(expected = IllegalArgumentException.class) @@ -88,6 +89,7 @@ public void buildWhenAllAttributesProvidedThenAllAttributesAreSet() { .tokenType(OAuth2AccessToken.TokenType.BEARER) .expiresIn(expiresAt.toEpochMilli()) .scopes(scopes) + .refreshToken(REFRESH_TOKEN_VALUE) .additionalParameters(additionalParameters) .build(); @@ -97,6 +99,58 @@ public void buildWhenAllAttributesProvidedThenAllAttributesAreSet() { assertThat(tokenResponse.getAccessToken().getIssuedAt()).isNotNull(); assertThat(tokenResponse.getAccessToken().getExpiresAt()).isAfterOrEqualTo(expiresAt); assertThat(tokenResponse.getAccessToken().getScopes()).isEqualTo(scopes); + assertThat(tokenResponse.getRefreshToken().getTokenValue()).isEqualTo(REFRESH_TOKEN_VALUE); assertThat(tokenResponse.getAdditionalParameters()).isEqualTo(additionalParameters); } + + @Test + public void buildWhenResponseThenAllAttributesAreSet() { + Instant expiresAt = Instant.now().plusSeconds(5); + Set scopes = new LinkedHashSet<>(Arrays.asList("scope1", "scope2")); + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + + OAuth2AccessTokenResponse tokenResponse = OAuth2AccessTokenResponse + .withToken(TOKEN_VALUE) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(expiresAt.toEpochMilli()) + .scopes(scopes) + .refreshToken(REFRESH_TOKEN_VALUE) + .additionalParameters(additionalParameters) + .build(); + + OAuth2AccessTokenResponse withResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) + .build(); + + assertThat(withResponse.getAccessToken().getTokenValue()).isEqualTo(tokenResponse.getAccessToken().getTokenValue()); + assertThat(withResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); + assertThat(withResponse.getAccessToken().getIssuedAt()).isEqualTo(tokenResponse.getAccessToken().getIssuedAt()); + assertThat(withResponse.getAccessToken().getExpiresAt()).isEqualTo(tokenResponse.getAccessToken().getExpiresAt()); + assertThat(withResponse.getAccessToken().getScopes()).isEqualTo(tokenResponse.getAccessToken().getScopes()); + assertThat(withResponse.getRefreshToken().getTokenValue()).isEqualTo(tokenResponse.getRefreshToken().getTokenValue()); + assertThat(withResponse.getAdditionalParameters()).isEqualTo(tokenResponse.getAdditionalParameters()); + } + + @Test + public void buildWhenResponseAndRefreshNullThenRefreshNull() { + Instant expiresAt = Instant.now().plusSeconds(5); + Set scopes = new LinkedHashSet<>(Arrays.asList("scope1", "scope2")); + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + + OAuth2AccessTokenResponse tokenResponse = OAuth2AccessTokenResponse + .withToken(TOKEN_VALUE) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(expiresAt.toEpochMilli()) + .scopes(scopes) + .additionalParameters(additionalParameters) + .build(); + + OAuth2AccessTokenResponse withResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) + .build(); + + assertThat(withResponse.getRefreshToken()).isNull(); + } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java index 70ccd4caf15..d61516785a1 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -16,9 +16,6 @@ package org.springframework.security.oauth2.core.endpoint; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.security.oauth2.core.AuthorizationGrantType; import java.util.Arrays; @@ -27,8 +24,7 @@ import java.util.Map; import java.util.Set; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.*; /** * Tests for {@link OAuth2AuthorizationRequest}. @@ -36,8 +32,6 @@ * @author Luander Ribeiro * @author Joe Grandja */ -@RunWith(PowerMockRunner.class) -@PrepareForTest(OAuth2AuthorizationRequest.class) public class OAuth2AuthorizationRequestTests { private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/authorize"; private static final String CLIENT_ID = "client-id"; @@ -45,59 +39,107 @@ public class OAuth2AuthorizationRequestTests { private static final Set SCOPES = new LinkedHashSet<>(Arrays.asList("scope1", "scope2")); private static final String STATE = "state"; - @Test(expected = IllegalArgumentException.class) + @Test public void buildWhenAuthorizationUriIsNullThenThrowIllegalArgumentException() { - OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(null) - .clientId(CLIENT_ID) - .redirectUri(REDIRECT_URI) - .scopes(SCOPES) - .state(STATE) - .build(); + assertThatThrownBy(() -> + OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(null) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .build() + ).isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void buildWhenClientIdIsNullThenThrowIllegalArgumentException() { - OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(null) - .redirectUri(REDIRECT_URI) - .scopes(SCOPES) - .state(STATE) - .build(); + assertThatThrownBy(() -> + OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(null) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .build() + ).isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void buildWhenRedirectUriIsNullForImplicitThenThrowIllegalArgumentException() { - OAuth2AuthorizationRequest.implicit() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(null) - .scopes(SCOPES) - .state(STATE) - .build(); + assertThatThrownBy(() -> + OAuth2AuthorizationRequest.implicit() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(null) + .scopes(SCOPES) + .state(STATE) + .build() + ).isInstanceOf(IllegalArgumentException.class); } @Test public void buildWhenRedirectUriIsNullForAuthorizationCodeThenDoesNotThrowAnyException() { - assertThatCode(() -> OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(null) - .scopes(SCOPES) - .state(STATE) - .build()).doesNotThrowAnyException(); + assertThatCode(() -> + OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(null) + .scopes(SCOPES) + .state(STATE) + .build()) + .doesNotThrowAnyException(); + } + + @Test + public void buildWhenScopesIsNullThenDoesNotThrowAnyException() { + assertThatCode(() -> + OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(null) + .state(STATE) + .build()) + .doesNotThrowAnyException(); + } + + @Test + public void buildWhenStateIsNullThenDoesNotThrowAnyException() { + assertThatCode(() -> + OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(null) + .build()) + .doesNotThrowAnyException(); + } + + @Test + public void buildWhenAdditionalParametersIsNullThenDoesNotThrowAnyException() { + assertThatCode(() -> + OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .additionalParameters(null) + .build()) + .doesNotThrowAnyException(); } @Test public void buildWhenImplicitThenGrantTypeResponseTypeIsSet() { OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.implicit() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(REDIRECT_URI) - .scopes(SCOPES) - .state(STATE) - .build(); + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .build(); assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.IMPLICIT); assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.TOKEN); } @@ -105,30 +147,31 @@ public void buildWhenImplicitThenGrantTypeResponseTypeIsSet() { @Test public void buildWhenAuthorizationCodeThenGrantTypeResponseTypeIsSet() { OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(null) - .scopes(SCOPES) - .state(STATE) - .build(); + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(null) + .scopes(SCOPES) + .state(STATE) + .build(); assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE); } @Test - public void buildWhenAllAttributesProvidedThenAllAttributesAreSet() { + public void buildWhenAllValuesProvidedThenAllValuesAreSet() { Map additionalParameters = new HashMap<>(); additionalParameters.put("param1", "value1"); additionalParameters.put("param2", "value2"); OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(REDIRECT_URI) - .scopes(SCOPES) - .state(STATE) - .additionalParameters(additionalParameters) - .build(); + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .additionalParameters(additionalParameters) + .authorizationRequestUri(AUTHORIZATION_URI) + .build(); assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI); assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); @@ -138,39 +181,113 @@ public void buildWhenAllAttributesProvidedThenAllAttributesAreSet() { assertThat(authorizationRequest.getScopes()).isEqualTo(SCOPES); assertThat(authorizationRequest.getState()).isEqualTo(STATE); assertThat(authorizationRequest.getAdditionalParameters()).isEqualTo(additionalParameters); + assertThat(authorizationRequest.getAuthorizationRequestUri()).isEqualTo(AUTHORIZATION_URI); } @Test - public void buildWhenScopesIsNullThenDoesNotThrowAnyException() { - assertThatCode(() -> OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(REDIRECT_URI) - .scopes(null) - .state(STATE) - .build()).doesNotThrowAnyException(); + public void buildWhenScopesMultiThenSeparatedByEncodedSpace() { + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.implicit() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .build(); + + assertThat(authorizationRequest.getAuthorizationRequestUri()).isEqualTo("https://provider.com/oauth2/authorize?response_type=token&client_id=client-id&scope=scope1+scope2&state=state&redirect_uri=http%3A%2F%2Fexample.com"); } @Test - public void buildWhenStateIsNullThenDoesNotThrowAnyException() { - assertThatCode(() -> OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(REDIRECT_URI) - .scopes(SCOPES) - .state(null) - .build()).doesNotThrowAnyException(); + public void buildWhenAuthorizationRequestUriSetThenOverridesDefault() { + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .authorizationRequestUri(AUTHORIZATION_URI) + .build(); + assertThat(authorizationRequest.getAuthorizationRequestUri()).isEqualTo(AUTHORIZATION_URI); } @Test - public void buildWhenAdditionalParametersIsNullThenDoesNotThrowAnyException() { - assertThatCode(() -> OAuth2AuthorizationRequest.authorizationCode() - .authorizationUri(AUTHORIZATION_URI) - .clientId(CLIENT_ID) - .redirectUri(REDIRECT_URI) - .scopes(SCOPES) - .state(STATE) - .additionalParameters(null) - .build()).doesNotThrowAnyException(); + public void buildWhenAuthorizationRequestUriNotSetThenDefaultSet() { + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .additionalParameters(additionalParameters) + .build(); + + assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull(); + assertThat(authorizationRequest.getAuthorizationRequestUri()).isEqualTo("https://provider.com/oauth2/authorize?response_type=code&client_id=client-id&scope=scope1+scope2&state=state&redirect_uri=http%3A%2F%2Fexample.com¶m1=value1¶m2=value2"); + } + + @Test + public void buildWhenRequiredParametersSetThenAuthorizationRequestUriIncludesRequiredParametersOnly() { + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .build(); + + assertThat(authorizationRequest.getAuthorizationRequestUri()).isEqualTo("https://provider.com/oauth2/authorize?response_type=code&client_id=client-id"); + } + + @Test + public void buildWhenAuthorizationRequestIncludesRegistrationIdParameterThenAuthorizationRequestUriDoesNotIncludeRegistrationIdParameter() { + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, "registration1"); + + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .additionalParameters(additionalParameters) + .build(); + + assertThat(authorizationRequest.getAuthorizationRequestUri()).isEqualTo("https://provider.com/oauth2/authorize?response_type=code&client_id=client-id&scope=scope1+scope2&state=state&redirect_uri=http%3A%2F%2Fexample.com¶m1=value1"); + } + + @Test + public void fromWhenAuthorizationRequestIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> OAuth2AuthorizationRequest.from(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void fromWhenAuthorizationRequestProvidedThenValuesAreCopied() { + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REDIRECT_URI) + .scopes(SCOPES) + .state(STATE) + .additionalParameters(additionalParameters) + .build(); + + OAuth2AuthorizationRequest authorizationRequestCopy = + OAuth2AuthorizationRequest.from(authorizationRequest).build(); + + assertThat(authorizationRequestCopy.getAuthorizationUri()).isEqualTo(authorizationRequest.getAuthorizationUri()); + assertThat(authorizationRequestCopy.getGrantType()).isEqualTo(authorizationRequest.getGrantType()); + assertThat(authorizationRequestCopy.getResponseType()).isEqualTo(authorizationRequest.getResponseType()); + assertThat(authorizationRequestCopy.getClientId()).isEqualTo(authorizationRequest.getClientId()); + assertThat(authorizationRequestCopy.getRedirectUri()).isEqualTo(authorizationRequest.getRedirectUri()); + assertThat(authorizationRequestCopy.getScopes()).isEqualTo(authorizationRequest.getScopes()); + assertThat(authorizationRequestCopy.getState()).isEqualTo(authorizationRequest.getState()); + assertThat(authorizationRequestCopy.getAdditionalParameters()).isEqualTo(authorizationRequest.getAdditionalParameters()); + assertThat(authorizationRequestCopy.getAuthorizationRequestUri()).isEqualTo(authorizationRequest.getAuthorizationRequestUri()); } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractorsTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractorsTests.java new file mode 100644 index 00000000000..8b9b63f01ee --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/web/reactive/function/OAuth2BodyExtractorsTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.reactive.function; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.codec.DecoderHttpMessageReader; +import org.springframework.http.codec.FormHttpMessageReader; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.mock.http.client.reactive.MockClientHttpResponse; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.web.reactive.function.BodyExtractor; +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class OAuth2BodyExtractorsTests { + + private BodyExtractor.Context context; + + private Map hints; + + @Before + public void createContext() { + final List> messageReaders = new ArrayList<>(); + messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); + messageReaders.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes())); + messageReaders.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); + messageReaders.add(new FormHttpMessageReader()); + + this.hints = new HashMap(); + this.context = new BodyExtractor.Context() { + @Override + public List> messageReaders() { + return messageReaders; + } + + @Override + public Optional serverResponse() { + return Optional.empty(); + } + + @Override + public Map hints() { + return OAuth2BodyExtractorsTests.this.hints; + } + }; + } + + @Test + public void oauth2AccessTokenResponseWhenInvalidJsonThenException() { + BodyExtractor, ReactiveHttpInputMessage> extractor = OAuth2BodyExtractors + .oauth2AccessTokenResponse(); + + MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK); + response.getHeaders().setContentType(MediaType.APPLICATION_JSON); + response.setBody("{"); + + Mono result = extractor.extract(response, this.context); + + assertThatCode(() -> result.block()) + .isInstanceOf(RuntimeException.class); + } + + @Test + public void oauth2AccessTokenResponseWhenValidThenCreated() throws Exception { + BodyExtractor, ReactiveHttpInputMessage> extractor = OAuth2BodyExtractors + .oauth2AccessTokenResponse(); + + MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK); + response.getHeaders().setContentType(MediaType.APPLICATION_JSON); + response.setBody("{\n" + + " \"access_token\":\"2YotnFZFEjr1zCsicMWpAA\",\n" + + " \"token_type\":\"Bearer\",\n" + + " \"expires_in\":3600,\n" + + " \"refresh_token\":\"tGzv3JOkF0XG5Qx2TlKWIA\",\n" + + " \"example_parameter\":\"example_value\"\n" + + " }"); + + Instant now = Instant.now(); + OAuth2AccessTokenResponse result = extractor.extract(response, this.context).block(); + + assertThat(result.getAccessToken().getTokenValue()).isEqualTo("2YotnFZFEjr1zCsicMWpAA"); + assertThat(result.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER); + assertThat(result.getAccessToken().getExpiresAt()).isBetween(now.plusSeconds(3600), now.plusSeconds(3600 + 2)); + assertThat(result.getRefreshToken().getTokenValue()).isEqualTo("tGzv3JOkF0XG5Qx2TlKWIA"); + assertThat(result.getAdditionalParameters()).containsEntry("example_parameter", "example_value"); + } +} diff --git a/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle b/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle index 7ad87dbc3ff..3336a2ffdd4 100644 --- a/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle +++ b/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle @@ -6,5 +6,10 @@ dependencies { compile springCoreDependency compile 'com.nimbusds:nimbus-jose-jwt' + optional 'io.projectreactor:reactor-core' + optional 'org.springframework:spring-webflux' + testCompile powerMock2Dependencies + testCompile 'com.squareup.okhttp3:mockwebserver' + testCompile 'io.projectreactor.netty:reactor-netty' } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContext.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContext.java new file mode 100644 index 00000000000..17a0ab20f31 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContext.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.util.Assert; + +import java.util.List; + +/** + * A {@link SecurityContext} that is used by {@link JWKContextJWKSource}. + * + * @author Rob Winch + * @since 5.1 + * @see JWKContextJWKSource + */ +class JWKContext implements SecurityContext { + private final List jwkList; + + JWKContext(List jwkList) { + Assert.notNull(jwkList, "jwkList cannot be null"); + this.jwkList = jwkList; + } + + public List getJwkList() { + return this.jwkList; + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContextJWKSource.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContextJWKSource.java new file mode 100644 index 00000000000..e444d39982a --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKContextJWKSource.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.source.JWKSource; + +import java.util.List; + +/** + * A {@link JWKSource} used for reactive applications that returns the {@link JWK} from the {@link JWKContext}. + * + *

      + * The Nimbus {@link JWKSource} is a blocking API which means the {@link JWK} cannot be resolved using code that blocks. + * This means that the JWK Set could not be retrieved from HTTP endpoint. To work around this the {@link JWK} is + * resolved in the {@link ReactiveJwtDecoder} and provided via the {@link JWKContext}. + *

      + * + * @author Rob Winch + * @since 5.1 + */ +class JWKContextJWKSource implements JWKSource { + + @Override + public List get(JWKSelector jwkSelector, JWKContext context) { + return context.getJwkList(); + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKSelectorFactory.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKSelectorFactory.java new file mode 100644 index 00000000000..1ba116fbf9f --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWKSelectorFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.KeySourceException; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; + +/** + * @author Rob Winch + * @since 5.1 + */ +class JWKSelectorFactory { + private final DelegateSelectorFactory delegate; + + JWKSelectorFactory(JWSAlgorithm expectedJWSAlgorithm) { + this.delegate = new DelegateSelectorFactory(expectedJWSAlgorithm); + } + + JWKSelector createSelector(JWSHeader jwsHeader) { + return new JWKSelector(this.delegate.createJWKMatcher(jwsHeader)); + } + + /** + * Used to expose the protected {@link #createJWKMatcher(JWSHeader)} method. + */ + private static class DelegateSelectorFactory extends JWSVerificationKeySelector { + /** + * Creates a new JWS verification key selector. + * + * @param jwsAlg The expected JWS algorithm for the objects to be + * verified. Must not be {@code null}. + */ + public DelegateSelectorFactory(JWSAlgorithm jwsAlg) { + super(jwsAlg, (jwkSelector, context) -> { + throw new KeySourceException("JWKSelectorFactory is only intended for creating a selector"); + }); + } + + @Override + public JWKMatcher createJWKMatcher(JWSHeader jwsHeader) { + return super.createJWKMatcher(jwsHeader); + } + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtIssuerValidator.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtIssuerValidator.java new file mode 100644 index 00000000000..6abd9a49459 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtIssuerValidator.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.MalformedURLException; +import java.net.URL; + +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.util.Assert; + +/** + * Validates the "iss" claim in a {@link Jwt}, that is matches a configured value + * + * @author Josh Cummings + * @since 5.1 + */ +public final class JwtIssuerValidator implements OAuth2TokenValidator { + private static OAuth2Error INVALID_ISSUER = + new OAuth2Error( + OAuth2ErrorCodes.INVALID_REQUEST, + "This iss claim is not equal to the configured issuer", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + + private final URL issuer; + + /** + * Constructs a {@link JwtIssuerValidator} using the provided parameters + * + * @param issuer - The issuer that each {@link Jwt} should have. + */ + public JwtIssuerValidator(String issuer) { + Assert.notNull(issuer, "issuer cannot be null"); + + try { + this.issuer = new URL(issuer); + } catch (MalformedURLException ex) { + throw new IllegalArgumentException( + "Invalid Issuer URL " + issuer + " : " + ex.getMessage(), + ex); + } + } + + /** + * {@inheritDoc} + */ + @Override + public OAuth2TokenValidatorResult validate(Jwt token) { + Assert.notNull(token, "token cannot be null"); + + if (this.issuer.equals(token.getIssuer())) { + return OAuth2TokenValidatorResult.success(); + } else { + return OAuth2TokenValidatorResult.failure(INVALID_ISSUER); + } + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTimestampValidator.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTimestampValidator.java new file mode 100644 index 00000000000..84ae6eb94d4 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTimestampValidator.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +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; +import org.springframework.util.Assert; + +/** + * An implementation of {@see OAuth2TokenValidator} for verifying claims in a Jwt-based access token + * + *

      + * Because clocks can differ between the Jwt source, say the Authorization Server, and its destination, say the + * Resource Server, there is a default clock leeway exercised when deciding if the current time is within the Jwt's + * specified operating window + * + * @author Josh Cummings + * @since 5.1 + * @see Jwt + * @see OAuth2TokenValidator + * @see JSON Web Token (JWT) + */ +public final class JwtTimestampValidator implements OAuth2TokenValidator { + private static final Duration DEFAULT_MAX_CLOCK_SKEW = Duration.of(60, ChronoUnit.SECONDS); + + private final Duration maxClockSkew; + + private Clock clock = Clock.systemUTC(); + + /** + * A basic instance with no custom verification and the default max clock skew + */ + public JwtTimestampValidator() { + this(DEFAULT_MAX_CLOCK_SKEW); + } + + public JwtTimestampValidator(Duration maxClockSkew) { + Assert.notNull(maxClockSkew, "maxClockSkew cannot be null"); + + this.maxClockSkew = maxClockSkew; + } + + /** + * {@inheritDoc} + */ + @Override + public OAuth2TokenValidatorResult validate(Jwt jwt) { + Assert.notNull(jwt, "jwt cannot be null"); + + Instant expiry = jwt.getExpiresAt(); + + if (expiry != null) { + if (Instant.now(this.clock).minus(maxClockSkew).isAfter(expiry)) { + OAuth2Error error = new OAuth2Error( + OAuth2ErrorCodes.INVALID_REQUEST, + String.format("Jwt expired at %s", jwt.getExpiresAt()), + "https://tools.ietf.org/html/rfc6750#section-3.1"); + return OAuth2TokenValidatorResult.failure(error); + } + } + + Instant notBefore = jwt.getNotBefore(); + + if (notBefore != null) { + if (Instant.now(this.clock).plus(maxClockSkew).isBefore(notBefore)) { + OAuth2Error error = new OAuth2Error( + OAuth2ErrorCodes.INVALID_REQUEST, + String.format("Jwt used before %s", jwt.getNotBefore()), + "https://tools.ietf.org/html/rfc6750#section-3.1"); + return OAuth2TokenValidatorResult.failure(error); + } + } + + return OAuth2TokenValidatorResult.success(); + } + + /** + * ' + * Use this {@link Clock} with {@link Instant#now()} for assessing + * timestamp validity + * + * @param clock + */ + public void setClock(Clock clock) { + Assert.notNull(clock, "clock cannot be null"); + this.clock = clock; + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidationException.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidationException.java new file mode 100644 index 00000000000..cb19e2bc338 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidationException.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.util.ArrayList; +import java.util.Collection; + +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.util.Assert; + +/** + * An exception that results from an unsuccessful + * {@link OAuth2TokenValidatorResult} + * + * @author Josh Cummings + * @since 5.1 + */ +public class JwtValidationException extends JwtException { + private final Collection errors; + + /** + * Constructs a {@link JwtValidationException} using the provided parameters + * + * While each {@link OAuth2Error} does contain an error description, this constructor + * can take an overarching description that encapsulates the composition of failures + * + * That said, it is appropriate to pass one of the messages from the error list in as + * the exception description, for example: + * + *

      +	 * 	if ( result.hasErrors() ) {
      +	 *  	Collection errors = result.getErrors();
      +	 *  	throw new JwtValidationException(errors.iterator().next().getDescription(), errors);
      +	 * 	}
      +	 * 
      + * + * @param message - the exception message + * @param errors - a list of {@link OAuth2Error}s with extra detail about the validation result + */ + public JwtValidationException(String message, Collection errors) { + super(message); + + Assert.notEmpty(errors, "errors cannot be empty"); + this.errors = new ArrayList<>(errors); + } + + /** + * Return the list of {@link OAuth2Error}s associated with this exception + * @return the list of {@link OAuth2Error}s associated with this exception + */ + public Collection getErrors() { + return this.errors; + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java new file mode 100644 index 00000000000..95667bbf445 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; + +/** + * Provides factory methods for creating {@code OAuth2TokenValidator} + * @author Josh Cummings + * @author Rob Winch + * @since 5.1 + */ +public final class JwtValidators { + + /** + *

      + * Create a {@link Jwt} Validator that contains all standard validators when an issuer is known. + *

      + *

      + * User's wanting to leverage the defaults plus additional validation can add the result of this + * method to {@code DelegatingOAuth2TokenValidator} along with the additional validators. + *

      + * @param issuer the issuer + * @return - a delegating validator containing all standard validators as well as any supplied + */ + public static OAuth2TokenValidator createDefaultWithIssuer(String issuer) { + List> validators = new ArrayList<>(); + validators.add(new JwtTimestampValidator()); + validators.add(new JwtIssuerValidator(issuer)); + return new DelegatingOAuth2TokenValidator<>(validators); + } + + /** + *

      + * Create a {@link Jwt} Validator that contains all standard validators. + *

      + *

      + * User's wanting to leverage the defaults plus additional validation can add the result of this + * method to {@code DelegatingOAuth2TokenValidator} along with the additional validators. + *

      + * @return - a delegating validator containing all standard validators as well as any supplied + */ + public static OAuth2TokenValidator createDefault() { + return new DelegatingOAuth2TokenValidator<>(Arrays.asList(new JwtTimestampValidator())); + } + + private JwtValidators() {} +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java index 6a8f0eb34f8..5dedcbeb7c3 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -15,30 +15,45 @@ */ package org.springframework.security.oauth2.jwt; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; +import java.time.Instant; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.RemoteKeySourceException; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.jwk.source.RemoteJWKSet; import com.nimbusds.jose.proc.JWSKeySelector; import com.nimbusds.jose.proc.JWSVerificationKeySelector; import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jose.util.DefaultResourceRetriever; +import com.nimbusds.jose.util.Resource; import com.nimbusds.jose.util.ResourceRetriever; import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; import org.springframework.util.Assert; - -import java.net.MalformedURLException; -import java.net.URL; -import java.time.Instant; -import java.util.LinkedHashMap; -import java.util.Map; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; /** - * An implementation of a {@link JwtDecoder} that "decodes" a + * An implementation of a {@link JwtDecoder} that "decodes" a * JSON Web Token (JWT) and additionally verifies it's digital signature if the JWT is a * JSON Web Signature (JWS). The public key used for verification is obtained from the * JSON Web Key (JWK) Set {@code URL} supplied via the constructor. @@ -47,6 +62,7 @@ * NOTE: This implementation uses the Nimbus JOSE + JWT SDK internally. * * @author Joe Grandja + * @author Josh Cummings * @since 5.0 * @see JwtDecoder * @see JSON Web Token (JWT) @@ -55,9 +71,14 @@ * @see Nimbus JOSE + JWT SDK */ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder { - private final URL jwkSetUrl; + private static final String DECODING_ERROR_MESSAGE_TEMPLATE = + "An error occurred while attempting to decode the Jwt: %s"; + private final JWSAlgorithm jwsAlgorithm; private final ConfigurableJWTProcessor jwtProcessor; + private final RestOperationsResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever(); + + private OAuth2TokenValidator jwtValidator = JwtValidators.createDefault(); /** * Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters. @@ -77,29 +98,54 @@ public NimbusJwtDecoderJwkSupport(String jwkSetUrl) { public NimbusJwtDecoderJwkSupport(String jwkSetUrl, String jwsAlgorithm) { Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty"); Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty"); + JWKSource jwkSource; try { - this.jwkSetUrl = new URL(jwkSetUrl); + jwkSource = new RemoteJWKSet(new URL(jwkSetUrl), this.jwkSetRetriever); } catch (MalformedURLException ex) { - throw new IllegalArgumentException("Invalid JWK Set URL " + jwkSetUrl + " : " + ex.getMessage(), ex); + throw new IllegalArgumentException("Invalid JWK Set URL \"" + jwkSetUrl + "\" : " + ex.getMessage(), ex); } this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm); - - ResourceRetriever jwkSetRetriever = new DefaultResourceRetriever(30000, 30000); - JWKSource jwkSource = new RemoteJWKSet(this.jwkSetUrl, jwkSetRetriever); JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource); - this.jwtProcessor = new DefaultJWTProcessor<>(); this.jwtProcessor.setJWSKeySelector(jwsKeySelector); + + // Spring Security validates the claim set independent from Nimbus + this.jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {}); } @Override public Jwt decode(String token) throws JwtException { - Jwt jwt; + JWT jwt = this.parse(token); + if (jwt instanceof SignedJWT) { + Jwt createdJwt = this.createJwt(token, jwt); + return this.validateJwt(createdJwt); + } + throw new JwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm()); + } + /** + * Use this {@link Jwt} Validator + * + * @param jwtValidator - the Jwt Validator to use + */ + public void setJwtValidator(OAuth2TokenValidator jwtValidator) { + Assert.notNull(jwtValidator, "jwtValidator cannot be null"); + this.jwtValidator = jwtValidator; + } + + private JWT parse(String token) { try { - JWT parsedJwt = JWTParser.parse(token); + return JWTParser.parse(token); + } catch (Exception ex) { + throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); + } + } + + private Jwt createJwt(String token, JWT parsedJwt) { + Jwt jwt; + try { // Verify the signature JWTClaimsSet jwtClaimsSet = this.jwtProcessor.process(parsedJwt, null); @@ -119,10 +165,67 @@ public Jwt decode(String token) throws JwtException { jwt = new Jwt(token, issuedAt, expiresAt, headers, jwtClaimsSet.getClaims()); + } catch (RemoteKeySourceException ex) { + if (ex.getCause() instanceof ParseException) { + throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed Jwk set")); + } else { + throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); + } } catch (Exception ex) { - throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); + if (ex.getCause() instanceof ParseException) { + throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed payload")); + } else { + throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex); + } } return jwt; } + + private Jwt validateJwt(Jwt jwt){ + OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt); + if (result.hasErrors()) { + String description = result.getErrors().iterator().next().getDescription(); + throw new JwtValidationException( + String.format(DECODING_ERROR_MESSAGE_TEMPLATE, description), + result.getErrors()); + } + + return jwt; + } + + /** + * Sets the {@link RestOperations} used when requesting the JSON Web Key (JWK) Set. + * + * @since 5.1 + * @param restOperations the {@link RestOperations} used when requesting the JSON Web Key (JWK) Set + */ + public final void setRestOperations(RestOperations restOperations) { + Assert.notNull(restOperations, "restOperations cannot be null"); + this.jwkSetRetriever.restOperations = restOperations; + } + + private static class RestOperationsResourceRetriever implements ResourceRetriever { + private RestOperations restOperations = new RestTemplate(); + + @Override + public Resource retrieveResource(URL url) throws IOException { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8)); + + ResponseEntity response; + try { + RequestEntity request = new RequestEntity<>(headers, HttpMethod.GET, url.toURI()); + response = this.restOperations.exchange(request, String.class); + } catch (Exception ex) { + throw new IOException(ex); + } + + 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 new file mode 100644 index 00000000000..67cb0f5e54c --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSelector; +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.BadJOSEException; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import com.nimbusds.jwt.proc.JWTProcessor; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; + +import java.security.interfaces.RSAPublicKey; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * An implementation of a {@link JwtDecoder} that "decodes" a + * JSON Web Token (JWT) and additionally verifies it's digital signature if the JWT is a + * JSON Web Signature (JWS). The public key used for verification is obtained from the + * JSON Web Key (JWK) Set {@code URL} supplied via the constructor. + * + *

      + * NOTE: This implementation uses the Nimbus JOSE + JWT SDK internally. + * + * @author Rob Winch + * @since 5.1 + * @see JwtDecoder + * @see JSON Web Token (JWT) + * @see JSON Web Signature (JWS) + * @see JSON Web Key (JWK) + * @see Nimbus JOSE + JWT SDK + */ +public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { + private final JWTProcessor jwtProcessor; + + private final ReactiveJWKSource reactiveJwkSource; + + private final JWKSelectorFactory jwkSelectorFactory; + + public NimbusReactiveJwtDecoder(RSAPublicKey publicKey) { + JWSAlgorithm algorithm = JWSAlgorithm.parse(JwsAlgorithms.RS256); + + RSAKey rsaKey = rsaKey(publicKey); + JWKSet jwkSet = new JWKSet(rsaKey); + JWKSource jwkSource = new ImmutableJWKSet<>(jwkSet); + JWSKeySelector jwsKeySelector = + new JWSVerificationKeySelector<>(algorithm, jwkSource); + DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWSKeySelector(jwsKeySelector); + + this.jwtProcessor = jwtProcessor; + this.reactiveJwkSource = new ReactiveJWKSourceAdapter(jwkSource); + this.jwkSelectorFactory = new JWKSelectorFactory(algorithm); + } + + /** + * Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters. + * + * @param jwkSetUrl the JSON Web Key (JWK) Set {@code URL} + */ + public NimbusReactiveJwtDecoder(String jwkSetUrl) { + Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty"); + String jwsAlgorithm = JwsAlgorithms.RS256; + JWSAlgorithm algorithm = JWSAlgorithm.parse(jwsAlgorithm); + JWKSource jwkSource = new JWKContextJWKSource(); + JWSKeySelector jwsKeySelector = + new JWSVerificationKeySelector<>(algorithm, jwkSource); + + DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWSKeySelector(jwsKeySelector); + this.jwtProcessor = jwtProcessor; + + this.reactiveJwkSource = new ReactiveRemoteJWKSource(jwkSetUrl); + + this.jwkSelectorFactory = new JWKSelectorFactory(algorithm); + + } + + @Override + public Mono decode(String token) throws JwtException { + JWT jwt = parse(token); + if (jwt instanceof SignedJWT) { + return this.decode((SignedJWT) jwt); + } + throw new JwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm()); + } + + private JWT parse(String token) { + try { + return JWTParser.parse(token); + } catch (Exception ex) { + throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); + } + } + + private Mono decode(SignedJWT parsedToken) { + try { + JWKSelector selector = this.jwkSelectorFactory + .createSelector(parsedToken.getHeader()); + return this.reactiveJwkSource.get(selector) + .onErrorMap(e -> new IllegalStateException("Could not obtain the keys", e)) + .map(jwkList -> createClaimsSet(parsedToken, jwkList)) + .map(set -> createJwt(parsedToken, set)) + .onErrorMap(e -> !(e instanceof IllegalStateException), e -> new JwtException("An error occurred while attempting to decode the Jwt: ", e)); + } catch (RuntimeException ex) { + throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); + } + } + + private JWTClaimsSet createClaimsSet(JWT parsedToken, List jwkList) { + try { + return this.jwtProcessor.process(parsedToken, new JWKContext(jwkList)); + } + catch (BadJOSEException | JOSEException e) { + throw new JwtException("Failed to validate the token", e); + } + } + + private Jwt createJwt(JWT parsedJwt, JWTClaimsSet jwtClaimsSet) { + Instant expiresAt = null; + if (jwtClaimsSet.getExpirationTime() != null) { + expiresAt = jwtClaimsSet.getExpirationTime().toInstant(); + } + Instant issuedAt = null; + if (jwtClaimsSet.getIssueTime() != null) { + issuedAt = jwtClaimsSet.getIssueTime().toInstant(); + } else if (expiresAt != null) { + // Default to expiresAt - 1 second + issuedAt = Instant.from(expiresAt).minusSeconds(1); + } + + Map headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject()); + + return new Jwt(parsedJwt.getParsedString(), issuedAt, expiresAt, headers, jwtClaimsSet.getClaims()); + } + + private static RSAKey rsaKey(RSAPublicKey publicKey) { + return new RSAKey.Builder(publicKey) + .build(); + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSource.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSource.java new file mode 100644 index 00000000000..32b84b88bc9 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSource.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSelector; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * A reactive version of {@link com.nimbusds.jose.jwk.source.JWKSource} + * @author Rob Winch + * @since 5.1 + */ +interface ReactiveJWKSource { + Mono> get(JWKSelector jwkSelector); +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSourceAdapter.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSourceAdapter.java new file mode 100644 index 00000000000..309ae578cd2 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJWKSourceAdapter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * Adapts a {@link JWKSource} to a {@link ReactiveJWKSource} which must be non-blocking. + * @author Rob Winch + * @since 5.1 + */ +class ReactiveJWKSourceAdapter implements ReactiveJWKSource { + private final JWKSource source; + + /** + * Creates a new instance + * @param source + */ + ReactiveJWKSourceAdapter(JWKSource source) { + this.source = source; + } + + @Override + public Mono> get(JWKSelector jwkSelector) { + return Mono.fromCallable(() -> this.source.get(jwkSelector, null)); + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoder.java new file mode 100644 index 00000000000..8ecf5d3e631 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoder.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 reactor.core.publisher.Mono; + +/** + * Implementations of this interface are responsible for "decoding" + * a JSON Web Token (JWT) from it's compact claims representation format to a {@link Jwt}. + * + *

      + * JWTs may be represented using the JWS Compact Serialization format for a + * JSON Web Signature (JWS) structure or JWE Compact Serialization format for a + * JSON Web Encryption (JWE) structure. Therefore, implementors are responsible + * for verifying a JWS and/or decrypting a JWE. + * + * @author Rob Winch + * @since 5.1 + * @see Jwt + * @see JSON Web Token (JWT) + * @see JSON Web Signature (JWS) + * @see JSON Web Encryption (JWE) + * @see JWS Compact Serialization + * @see JWE Compact Serialization + */ +public interface ReactiveJwtDecoder { + + /** + * Decodes the JWT from it's compact claims representation format and returns a {@link Jwt}. + * + * @param token the JWT value + * @return a {@link Jwt} + * @throws JwtException if an error occurs while attempting to decode the JWT + */ + Mono decode(String token) throws JwtException; + +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java new file mode 100644 index 00000000000..0079c8b8634 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 com.nimbusds.jose.RemoteKeySourceException; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.JWKSet; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.text.ParseException; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author Rob Winch + * @since 5.1 + */ +class ReactiveRemoteJWKSource implements ReactiveJWKSource { + /** + * The cached JWK set. + */ + private final AtomicReference> cachedJWKSet = new AtomicReference<>(Mono.empty()); + + private WebClient webClient = WebClient.create(); + + private final String jwkSetURL; + + ReactiveRemoteJWKSource(String jwkSetURL) { + this.jwkSetURL = jwkSetURL; + } + + public Mono> get(JWKSelector jwkSelector) { + return this.cachedJWKSet.get() + .switchIfEmpty(getJWKSet()) + .flatMap(jwkSet -> get(jwkSelector, jwkSet)) + .switchIfEmpty(getJWKSet().map(jwkSet -> jwkSelector.select(jwkSet))); + } + + private Mono> get(JWKSelector jwkSelector, JWKSet jwkSet) { + return Mono.defer(() -> { + // Run the selector on the JWK set + List matches = jwkSelector.select(jwkSet); + + if (!matches.isEmpty()) { + // Success + return Mono.just(matches); + } + + // Refresh the JWK set if the sought key ID is not in the cached JWK set + + // Looking for JWK with specific ID? + String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher()); + if (soughtKeyID == null) { + // No key ID specified, return no matches + return Mono.just(Collections.emptyList()); + } + + if (jwkSet.getKeyByKeyId(soughtKeyID) != null) { + // The key ID exists in the cached JWK set, matching + // failed for some other reason, return no matches + return Mono.just(Collections.emptyList()); + } + + return Mono.empty(); + + }); + } + + /** + * Updates the cached JWK set from the configured URL. + * + * @return The updated JWK set. + * + * @throws RemoteKeySourceException If JWK retrieval failed. + */ + private Mono getJWKSet() { + return this.webClient.get() + .uri(this.jwkSetURL) + .retrieve() + .bodyToMono(String.class) + .map(this::parse) + .doOnNext(jwkSet -> this.cachedJWKSet.set(Mono.just(jwkSet))) + .cache(); + } + + private JWKSet parse(String body) { + try { + return JWKSet.parse(body); + } + catch (ParseException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the first specified key ID (kid) for a JWK matcher. + * + * @param jwkMatcher The JWK matcher. Must not be {@code null}. + * + * @return The first key ID, {@code null} if none. + */ + protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) { + + Set keyIDs = jwkMatcher.getKeyIDs(); + + if (keyIDs == null || keyIDs.isEmpty()) { + return null; + } + + for (String id: keyIDs) { + if (id != null) { + return id; + } + } + return null; // No kid in matcher + } +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextJWKSourceTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextJWKSourceTests.java new file mode 100644 index 00000000000..1da696bfdf7 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextJWKSourceTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 com.nimbusds.jose.jwk.JWK; +import org.junit.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.mock; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class JWKContextJWKSourceTests { + private JWKContextJWKSource source = new JWKContextJWKSource(); + + @Test + public void getWhenKeysNotEmptyThenContainsKeys() { + JWK key = mock(JWK.class); + JWKContext jwkContext = new JWKContext(Arrays.asList(key)); + + assertThat(this.source.get(null, jwkContext)).containsOnly(key); + } + +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextTests.java new file mode 100644 index 00000000000..cce04d9bd43 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JWKContextTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 com.nimbusds.jose.jwk.JWK; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.mock; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class JWKContextTests { + + @Test + public void constructorWhenNullThenIllegalArgumentException() { + List jwkList = null; + assertThatCode(() -> new JWKContext(jwkList)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void getJwkListWhenEmpty() { + JWKContext jwkContext = new JWKContext(Collections.emptyList()); + assertThat(jwkContext.getJwkList()).isEmpty(); + } + + @Test + public void getJwkListWhenNotEmpty() { + JWK key = mock(JWK.class); + JWKContext jwkContext = new JWKContext(Arrays.asList(key)); + assertThat(jwkContext.getJwkList()).containsOnly(key); + } +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtIssuerValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtIssuerValidatorTests.java new file mode 100644 index 00000000000..7a01da149e2 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtIssuerValidatorTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.time.Instant; +import java.util.Collections; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtIssuerValidator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Josh Cummings + * @since 5.1 + */ +public class JwtIssuerValidatorTests { + private static final String MOCK_TOKEN = "token"; + private static final Instant MOCK_ISSUED_AT = Instant.MIN; + private static final Instant MOCK_EXPIRES_AT = Instant.MAX; + private static final Map MOCK_HEADERS = + Collections.singletonMap("alg", JwsAlgorithms.RS256); + + private static final String ISSUER = "https://issuer"; + + private final JwtIssuerValidator validator = new JwtIssuerValidator(ISSUER); + + @Test + public void validateWhenIssuerMatchesThenReturnsSuccess() { + Jwt jwt = new Jwt( + MOCK_TOKEN, + MOCK_ISSUED_AT, + MOCK_EXPIRES_AT, + MOCK_HEADERS, + Collections.singletonMap("iss", ISSUER)); + + assertThat(this.validator.validate(jwt)) + .isEqualTo(OAuth2TokenValidatorResult.success()); + } + + @Test + public void validateWhenIssuerMismatchesThenReturnsError() { + Jwt jwt = new Jwt( + MOCK_TOKEN, + MOCK_ISSUED_AT, + MOCK_EXPIRES_AT, + MOCK_HEADERS, + Collections.singletonMap(JwtClaimNames.ISS, "https://other")); + + OAuth2TokenValidatorResult result = this.validator.validate(jwt); + + assertThat(result.getErrors()).isNotEmpty(); + } + + @Test + public void validateWhenJwtIsNullThenThrowsIllegalArgumentException() { + assertThatCode(() -> this.validator.validate(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenMalformedIssuerIsGivenThenThrowsIllegalArgumentException() { + assertThatCode(() -> new JwtIssuerValidator("issuer")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenNullIssuerIsGivenThenThrowsIllegalArgumentException() { + assertThatCode(() -> new JwtIssuerValidator(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java new file mode 100644 index 00000000000..57fe14b1257 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.Test; + +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtTimestampValidator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests verifying {@link JwtTimestampValidator} + * + * @author Josh Cummings + */ +public class JwtTimestampValidatorTests { + private static final Clock MOCK_NOW = Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault()); + private static final String MOCK_TOKEN_VALUE = "token"; + private static final Instant MOCK_ISSUED_AT = Instant.MIN; + private static final Map MOCK_HEADER = Collections.singletonMap("alg", JwsAlgorithms.RS256); + private static final Map MOCK_CLAIM_SET = Collections.singletonMap("some", "claim"); + + @Test + public void validateWhenJwtIsExpiredThenErrorMessageIndicatesExpirationTime() { + Instant oneHourAgo = Instant.now().minusSeconds(3600); + + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + oneHourAgo, + MOCK_HEADER, + MOCK_CLAIM_SET); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + + Collection details = jwtValidator.validate(jwt).getErrors(); + Collection messages = details.stream().map(OAuth2Error::getDescription).collect(Collectors.toList()); + + assertThat(messages).contains("Jwt expired at " + oneHourAgo); + } + + @Test + public void validateWhenJwtIsTooEarlyThenErrorMessageIndicatesNotBeforeTime() { + Instant oneHourFromNow = Instant.now().plusSeconds(3600); + + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + null, + MOCK_HEADER, + Collections.singletonMap(JwtClaimNames.NBF, oneHourFromNow)); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + + Collection details = jwtValidator.validate(jwt).getErrors(); + Collection messages = details.stream().map(OAuth2Error::getDescription).collect(Collectors.toList()); + + assertThat(messages).contains("Jwt used before " + oneHourFromNow); + } + + @Test + public void validateWhenConfiguredWithClockSkewThenValidatesUsingThatSkew() { + Duration oneDayOff = Duration.ofDays(1); + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(oneDayOff); + + Instant now = Instant.now(); + Instant almostOneDayAgo = now.minus(oneDayOff).plusSeconds(10); + Instant almostOneDayFromNow = now.plus(oneDayOff).minusSeconds(10); + Instant justOverOneDayAgo = now.minus(oneDayOff).minusSeconds(10); + Instant justOverOneDayFromNow = now.plus(oneDayOff).plusSeconds(10); + + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + almostOneDayAgo, + MOCK_HEADER, + Collections.singletonMap(JwtClaimNames.NBF, almostOneDayFromNow)); + + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + + jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + justOverOneDayAgo, + MOCK_HEADER, + MOCK_CLAIM_SET); + + OAuth2TokenValidatorResult result = jwtValidator.validate(jwt); + Collection messages = + result.getErrors().stream().map(OAuth2Error::getDescription).collect(Collectors.toList()); + + assertThat(result.hasErrors()).isTrue(); + assertThat(messages).contains("Jwt expired at " + justOverOneDayAgo); + + jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + null, + MOCK_HEADER, + Collections.singletonMap(JwtClaimNames.NBF, justOverOneDayFromNow)); + + result = jwtValidator.validate(jwt); + messages = + result.getErrors().stream().map(OAuth2Error::getDescription).collect(Collectors.toList()); + + assertThat(result.hasErrors()).isTrue(); + assertThat(messages).contains("Jwt used before " + justOverOneDayFromNow); + + } + + @Test + public void validateWhenConfiguredWithFixedClockThenValidatesUsingFixedTime() { + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + Instant.now(MOCK_NOW), + MOCK_HEADER, + Collections.singletonMap("some", "claim")); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofNanos(0)); + jwtValidator.setClock(MOCK_NOW); + + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + + jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + null, + MOCK_HEADER, + Collections.singletonMap(JwtClaimNames.NBF, Instant.now(MOCK_NOW))); + + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + } + + @Test + public void validateWhenNeitherExpiryNorNotBeforeIsSpecifiedThenReturnsSuccessfulResult() { + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + null, + MOCK_HEADER, + MOCK_CLAIM_SET); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + } + + @Test + public void validateWhenNotBeforeIsValidAndExpiryIsNotSpecifiedThenReturnsSuccessfulResult() { + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + null, + MOCK_HEADER, + Collections.singletonMap(JwtClaimNames.NBF, Instant.MIN)); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + } + + @Test + public void validateWhenExpiryIsValidAndNotBeforeIsNotSpecifiedThenReturnsSuccessfulResult() { + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + Instant.MAX, + MOCK_HEADER, + MOCK_CLAIM_SET); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + } + + @Test + public void validateWhenBothExpiryAndNotBeforeAreValidThenReturnsSuccessfulResult() { + Jwt jwt = new Jwt( + MOCK_TOKEN_VALUE, + MOCK_ISSUED_AT, + Instant.now(MOCK_NOW), + MOCK_HEADER, + Collections.singletonMap(JwtClaimNames.NBF, Instant.now(MOCK_NOW))); + + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofNanos(0)); + jwtValidator.setClock(MOCK_NOW); + + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + } + + @Test + public void setClockWhenInvokedWithNullThenThrowsIllegalArgumentException() { + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + + assertThatCode(() -> jwtValidator.setClock(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenInvokedWithNullDurationThenThrowsIllegalArgumentException() { + assertThatCode(() -> new JwtTimestampValidator(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java index 177a52f91c4..bc3f36932e6 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -15,35 +15,65 @@ */ package org.springframework.security.oauth2.jwt; +import java.util.Arrays; + import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.assertj.core.api.Assertions; import org.junit.Test; import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; + +import org.springframework.http.RequestEntity; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.powermock.api.mockito.PowerMockito.*; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.spy; +import static org.powermock.api.mockito.PowerMockito.when; +import static org.powermock.api.mockito.PowerMockito.whenNew; /** * Tests for {@link NimbusJwtDecoderJwkSupport}. * * @author Joe Grandja + * @author Josh Cummings */ @RunWith(PowerMockRunner.class) @PrepareForTest({NimbusJwtDecoderJwkSupport.class, JWTParser.class}) +@PowerMockIgnore("okhttp3.*") public class NimbusJwtDecoderJwkSupportTests { private static final String JWK_SET_URL = "https://provider.com/oauth2/keys"; private static final String JWS_ALGORITHM = JwsAlgorithms.RS256; + 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 MALFORMED_JWK_SET = "malformed"; + + private static final String SIGNED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTpyZWFkIl0sImV4cCI6NDY4Mzg5Nzc3Nn0.LtMVtIiRIwSyc3aX35Zl0JVwLTcQZAB3dyBOMHNaHCKUljwMrf20a_gT79LfhjDzE_fUVUmFiAO32W1vFnYpZSVaMDUgeIOIOpxfoe9shj_uYenAwIS-_UxqGVIJiJoXNZh_MK80ShNpvsQwamxWEEOAMBtpWNiVYNDMdfgho9n3o5_Z7Gjy8RLBo1tbDREbO9kTFwGIxm_EYpezmRCRq4w1DdS6UDW321hkwMxPnCMSWOvp-hRpmgY2yjzLgPJ6Aucmg9TJ8jloAP1DjJoF1gRR7NTAk8LOGkSjTzVYDYMbCF51YdpojhItSk80YzXiEsv1mTz4oMM49jXBmfXFMA"; + private static final String MALFORMED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODQyMjUwODd9.guoQvujdWvd3xw7FYQEn4D6-gzM_WqFvXdmvAUNSLbxG7fv2_LLCNujPdrBHJoYPbOwS1BGNxIKQWS1tylvqzmr1RohQ-RZ2iAM1HYQzboUlkoMkcd8ENM__ELqho8aNYBfqwkNdUOyBFoy7Syu_w2SoJADw2RTjnesKO6CVVa05bW118pDS4xWxqC4s7fnBjmZoTn4uQ-Kt9YSQZQk8YQxkJSiyanozzgyfgXULA6mPu1pTNU3FVFaK1i1av_xtH_zAPgb647ZeaNe4nahgqC5h8nhOlm8W2dndXbwAt29nd2ZWBsru_QwZz83XSKLhTPFz-mPBByZZDsyBbIHf9A"; + private static final String UNSIGNED_JWT = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9."; + + private NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM); + @Test public void constructorWhenJwkSetUrlIsNullThenThrowIllegalArgumentException() { assertThatThrownBy(() -> new NimbusJwtDecoderJwkSupport(null)) @@ -62,17 +92,22 @@ public void constructorWhenJwsAlgorithmIsNullThenThrowIllegalArgumentException() .isInstanceOf(IllegalArgumentException.class); } + @Test + public void setRestOperationsWhenNullThenThrowIllegalArgumentException() { + Assertions.assertThatThrownBy(() -> this.jwtDecoder.setRestOperations(null)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void decodeWhenJwtInvalidThenThrowJwtException() { - NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM); - assertThatThrownBy(() -> jwtDecoder.decode("invalid")) + assertThatThrownBy(() -> this.jwtDecoder.decode("invalid")) .isInstanceOf(JwtException.class); } // gh-5168 @Test public void decodeWhenExpClaimNullThenDoesNotThrowException() throws Exception { - JWT jwt = mock(JWT.class); + SignedJWT jwt = mock(SignedJWT.class); JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(JWS_ALGORITHM)).build(); when(jwt.getHeader()).thenReturn(header); @@ -85,7 +120,112 @@ public void decodeWhenExpClaimNullThenDoesNotThrowException() throws Exception { JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().audience("resource1").build(); when(jwtProcessor.process(any(JWT.class), eq(null))).thenReturn(jwtClaimsSet); - NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM); + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL); assertThatCode(() -> jwtDecoder.decode("encoded-jwt")).doesNotThrowAnyException(); } + + // gh-5457 + @Test + public void decodeWhenPlainJwtThenExceptionDoesNotMentionClass() { + assertThatCode(() -> this.jwtDecoder.decode(UNSIGNED_JWT)) + .isInstanceOf(JwtException.class) + .hasMessageContaining("Unsupported algorithm of none"); + } + + @Test + public void decodeWhenJwtIsMalformedThenReturnsStockException() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + server.enqueue(new MockResponse().setBody(JWK_SET)); + String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + assertThatCode(() -> jwtDecoder.decode(MALFORMED_JWT)) + .isInstanceOf(JwtException.class) + .hasMessage("An error occurred while attempting to decode the Jwt: Malformed payload"); + server.shutdown(); + } + } + + @Test + public void decodeWhenJwkResponseIsMalformedThenReturnsStockException() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET)); + String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT)) + .isInstanceOf(JwtException.class) + .hasMessage("An error occurred while attempting to decode the Jwt: Malformed Jwk set"); + server.shutdown(); + } + } + + @Test + public void decodeWhenJwkEndpointIsUnresponsiveThenReturnsJwtException() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET)); + String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT)) + .isInstanceOf(JwtException.class) + .hasMessageContaining("An error occurred while attempting to decode the Jwt"); + server.shutdown(); + } + } + + // gh-5603 + @Test + public void decodeWhenCustomRestOperationsSetThenUsed() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + server.enqueue(new MockResponse().setBody(JWK_SET)); + String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); + NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + RestTemplate restTemplate = spy(new RestTemplate()); + jwtDecoder.setRestOperations(restTemplate); + assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT)).doesNotThrowAnyException(); + verify(restTemplate).exchange(any(RequestEntity.class), eq(String.class)); + server.shutdown(); + } + } + + @Test + public void decodeWhenJwtFailsValidationThenReturnsCorrespondingErrorMessage() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + server.enqueue(new MockResponse().setBody(JWK_SET)); + String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); + + NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + + OAuth2Error failure = new OAuth2Error("mock-error", "mock-description", "mock-uri"); + + OAuth2TokenValidator jwtValidator = mock(OAuth2TokenValidator.class); + when(jwtValidator.validate(any(Jwt.class))).thenReturn(OAuth2TokenValidatorResult.failure(failure)); + decoder.setJwtValidator(jwtValidator); + + assertThatCode(() -> decoder.decode(SIGNED_JWT)) + .isInstanceOf(JwtValidationException.class) + .hasMessageContaining("mock-description"); + } + } + + @Test + public void decodeWhenJwtValidationHasTwoErrorsThenJwtExceptionMessageShowsFirstError() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + server.enqueue(new MockResponse().setBody(JWK_SET)); + String jwkSetUrl = server.url("/.well-known/jwks.json").toString(); + + NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl); + + OAuth2Error firstFailure = new OAuth2Error("mock-error", "mock-description", "mock-uri"); + OAuth2Error secondFailure = new OAuth2Error("another-error", "another-description", "another-uri"); + OAuth2TokenValidatorResult result = OAuth2TokenValidatorResult.failure(firstFailure, secondFailure); + + OAuth2TokenValidator jwtValidator = mock(OAuth2TokenValidator.class); + when(jwtValidator.validate(any(Jwt.class))).thenReturn(result); + decoder.setJwtValidator(jwtValidator); + + assertThatCode(() -> decoder.decode(SIGNED_JWT)) + .isInstanceOf(JwtValidationException.class) + .hasMessageContaining("mock-description") + .hasFieldOrPropertyWithValue("errors", Arrays.asList(firstFailure, secondFailure)); + } + } } 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 new file mode 100644 index 00000000000..2b3ca53adf6 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.net.UnknownHostException; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class NimbusReactiveJwtDecoderTests { + + private String expired = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6MTUyOTkzNzYzMX0.Dt5jFOKkB8zAmjciwvlGkj4LNStXWH0HNIfr8YYajIthBIpVgY5Hg_JL8GBmUFzKDgyusT0q60OOg8_Pdi4Lu-VTWyYutLSlNUNayMlyBaVEWfyZJnh2_OwMZr1vRys6HF-o1qZldhwcfvczHg61LwPa1ISoqaAltDTzBu9cGISz2iBUCuR0x71QhbuRNyJdjsyS96NqiM_TspyiOSxmlNch2oAef1MssOQ23CrKilIvEDsz_zk5H94q7rH0giWGdEHCENESsTJS0zvzH6r2xIWjd5WnihFpCPkwznEayxaEhrdvJqT_ceyXCIfY4m3vujPQHNDG0UshpwvDuEbPUg"; + + private String messageReadToken = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.bnQ8IJDXmQbmIXWku0YT1HOyV_3d0iQSA_0W2CmPyELhsxFETzBEEcZ0v0xCBiswDT51rwD83wbX3YXxb84fM64AhpU8wWOxLjha4J6HJX2JnlG47ydaAVD7eWGSYTavyyQ-CwUjQWrfMVcObFZLYG11ydzRYOR9-aiHcK3AobcTcS8jZFeI8EGQV_Cd3IJ018uFCf6VnXLv7eV2kRt08Go2RiPLW47ExvD7Dzzz_wDBKfb4pNem7fDvuzB3UPcp5m9QvLZicnbS_6AvDi6P1y_DFJf-1T5gkGmX5piDH1L1jg2Yl6tjmXbk5B3VhsyjJuXE6gzq1d-xie0Z1NVOxw"; + + private String unsignedToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9."; + + private String jwkSet = + "{\n" + + " \"keys\":[\n" + + " {\n" + + " \"kty\":\"RSA\",\n" + + " \"e\":\"AQAB\",\n" + + " \"use\":\"sig\",\n" + + " \"kid\":\"key-id-1\",\n" + + " \"n\":\"qL48v1clgFw-Evm145pmh8nRYiNt72Gupsshn7Qs8dxEydCRp1DPOV_PahPk1y2nvldBNIhfNL13JOAiJ6BTiF-2ICuICAhDArLMnTH61oL1Hepq8W1xpa9gxsnL1P51thvfmiiT4RTW57koy4xIWmIp8ZXXfYgdH2uHJ9R0CQBuYKe7nEOObjxCFWC8S30huOfW2cYtv0iB23h6w5z2fDLjddX6v_FXM7ktcokgpm3_XmvT_-bL6_GGwz9k6kJOyMTubecr-WT__le8ikY66zlplYXRQh6roFfFCL21Pt8xN5zrk-0AMZUnmi8F2S2ztSBmAVJ7H71ELXsURBVZpw\"\n" + + " }\n" + + " ]\n" + + "}"; + + private MockWebServer server; + private NimbusReactiveJwtDecoder decoder; + + @Before + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.server.enqueue(new MockResponse().setBody(jwkSet)); + this.decoder = new NimbusReactiveJwtDecoder(this.server.url("/certs").toString()); + } + + @After + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void decodeWhenInvalidUrl() { + this.decoder = new NimbusReactiveJwtDecoder("https://s"); + + assertThatCode(() -> this.decoder.decode(this.messageReadToken).block()) + .isInstanceOf(IllegalStateException.class) + .hasCauseInstanceOf(UnknownHostException.class); + + } + + @Test + public void decodeWhenMessageReadScopeThenSuccess() { + Jwt jwt = this.decoder.decode(this.messageReadToken).block(); + + assertThat(jwt.getClaims().get("scope")).isEqualTo("message:read"); + } + + @Test + public void decodeWhenRSAPublicKeyThenSuccess() throws Exception { + byte[] bytes = Base64.getDecoder().decode("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqL48v1clgFw+Evm145pmh8nRYiNt72Gupsshn7Qs8dxEydCRp1DPOV/PahPk1y2nvldBNIhfNL13JOAiJ6BTiF+2ICuICAhDArLMnTH61oL1Hepq8W1xpa9gxsnL1P51thvfmiiT4RTW57koy4xIWmIp8ZXXfYgdH2uHJ9R0CQBuYKe7nEOObjxCFWC8S30huOfW2cYtv0iB23h6w5z2fDLjddX6v/FXM7ktcokgpm3/XmvT/+bL6/GGwz9k6kJOyMTubecr+WT//le8ikY66zlplYXRQh6roFfFCL21Pt8xN5zrk+0AMZUnmi8F2S2ztSBmAVJ7H71ELXsURBVZpwIDAQAB"); + RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(bytes)); + this.decoder = new NimbusReactiveJwtDecoder(publicKey); + String noKeyId = "eyJhbGciOiJSUzI1NiJ9.eyJzY29wZSI6IiIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.hNVuHSUkxdLZrDfqdmKcOi0ggmNaDuB4ZPxPtJl1gwBiXzIGN6Hwl24O2BfBZiHFKUTQDs4_RvzD71mEG3DvUrcKmdYWqIB1l8KNmxQLUDG-cAPIpJmRJgCh50tf8OhOE_Cb9E1HcsOUb47kT9iz-VayNBcmo6BmyZLdEGhsdGBrc3Mkz2dd_0PF38I2Hf_cuSjn9gBjFGtiPEXJvob3PEjVTSx_zvodT8D9p3An1R3YBZf5JSd1cQisrXgDX2k1Jmf7UKKWzgfyCgnEtRWWbsUdPqo3rSEY9GDC1iSQXsFTTC1FT_JJDkwzGf011fsU5O_Ko28TARibmKTCxAKNRQ"; + + assertThatCode(() -> this.decoder.decode(noKeyId).block()) + .doesNotThrowAnyException(); + } + + @Test + 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().get(JwtClaimNames.IAT)).isEqualTo(new Date(1529942448000L)); + } + + @Test + public void decodeWhenExpiredThenFail() { + assertThatCode(() -> this.decoder.decode(this.expired).block()) + .isInstanceOf(JwtException.class); + } + + @Test + public void decodeWhenNoPeriodThenFail() { + assertThatCode(() -> this.decoder.decode("").block()) + .isInstanceOf(JwtException.class); + } + + @Test + public void decodeWhenInvalidJwkSetUrlThenFail() { + this.decoder = new NimbusReactiveJwtDecoder("http://localhost:1280/certs"); + assertThatCode(() -> this.decoder.decode(this.messageReadToken).block()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void decodeWhenInvalidSignatureThenFail() { + assertThatCode(() -> this.decoder.decode(this.messageReadToken.substring(0, this.messageReadToken.length() - 2)).block()) + .isInstanceOf(JwtException.class); + } + + @Test + public void decodeWhenAlgNoneThenFail() { + assertThatCode(() -> this.decoder.decode("ew0KICAiYWxnIjogIm5vbmUiLA0KICAidHlwIjogIkpXVCINCn0.ew0KICAic3ViIjogIjEyMzQ1Njc4OTAiLA0KICAibmFtZSI6ICJKb2huIERvZSIsDQogICJpYXQiOiAxNTE2MjM5MDIyDQp9.").block()) + .isInstanceOf(JwtException.class) + .hasMessage("Unsupported algorithm of none"); + } + + @Test + public void decodeWhenInvalidAlgMismatchThenFail() { + assertThatCode(() -> this.decoder.decode("ew0KICAiYWxnIjogIkVTMjU2IiwNCiAgInR5cCI6ICJKV1QiDQp9.ew0KICAic3ViIjogIjEyMzQ1Njc4OTAiLA0KICAibmFtZSI6ICJKb2huIERvZSIsDQogICJpYXQiOiAxNTE2MjM5MDIyDQp9.").block()) + .isInstanceOf(JwtException.class); + } + + @Test + public void decodeWhenUnsignedTokenThenMessageDoesNotMentionClass() { + assertThatCode(() -> this.decoder.decode(this.unsignedToken).block()) + .isInstanceOf(JwtException.class) + .hasMessage("Unsupported algorithm of none"); + } +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSourceTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSourceTests.java new file mode 100644 index 00000000000..58b20a28363 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSourceTests.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.KeyType; +import com.nimbusds.jose.jwk.KeyUse; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class ReactiveRemoteJWKSourceTests { + @Mock + private JWKMatcher matcher; + + private ReactiveRemoteJWKSource source; + + private JWKSelector selector; + + private MockWebServer server; + + private String keys = "{\n" + + " \"keys\": [\n" + + " {\n" + + " \"alg\": \"RS256\", \n" + + " \"e\": \"AQAB\", \n" + + " \"kid\": \"1923397381d9574bb873202a90c32b7ceeaed027\", \n" + + " \"kty\": \"RSA\", \n" + + " \"n\": \"m4I5Dk5GnbzzUtqaljDVbpMONi1JLNJ8ZuXE8VvjCAVebDg5vTYhQ33jUwGgbn1wFmytUMgMmvK8A8Gpshl0sO2GBIZoh6_pwLrk657ZEtv-hx9fYKnzwyrfHqxtSswMAyr7XtKl8Ha1I03uFMSaYaaBTwVXCHByhzr4PVXfKAYJNbbcteUZfE8ODlBQkjQLI0IB78Nu8XIRrdzTF_5LCuM6rLUNtX6_KdzPpeX9KEtB7OBAfkdZEtBzGI-aYNLtIaL4qO6cVxBeVDLMoj9kVsRPylrwhEFQcGOjtJhwJwXFzTMZVhkiLFCHxZkkjoMrK5osSRlhduuGI9ot8XTUKQ\", \n" + + " \"use\": \"sig\"\n" + + " }, \n" + + " {\n" + + " \"alg\": \"RS256\", \n" + + " \"e\": \"AQAB\", \n" + + " \"kid\": \"7ddf54d3032d1f0d48c3618892ca74c1ac30ad77\", \n" + + " \"kty\": \"RSA\", \n" + + " \"n\": \"yLlYyux949b7qS-DdqTNjdZb4NtqiNH-Jt7DtRxmfW9XZLOQ6Q2NYgmPe9hyy5GHG7W3zsd6Q-rzq5eGRNEUx1767K1dS5PtkVWPiPG_M7rDqCu3HsLmKQKhRjHYaCWl5NuiMB5mXoPhSwrHd2yeGE7QHIV7_CiQFc1xQsXeiC-nTeJohJO3HI97w0GXE8pHspLYq9oG87f5IHxFr89abmwRug-D7QWQyW5b4doe4ZL-52J-8WHd52kGrGfu4QyV83oAad3I_9Q-yiWOXUr_0GIrzz4_-u5HgqYexnodFhZZSaKuRSg_b5qCnPhW8gBDLAHkmQzQMaWsN14L0pokbQ\", \n" + + " \"use\": \"sig\"\n" + + " }\n" + + " ]\n" + + "}\n"; + + + private String keys2 = "{\n" + + " \"keys\": [\n" + + " {\n" + + " \"alg\": \"RS256\", \n" + + " \"e\": \"AQAB\", \n" + + " \"kid\": \"rotated\", \n" + + " \"kty\": \"RSA\", \n" + + " \"n\": \"m4I5Dk5GnbzzUtqaljDVbpMONi1JLNJ8ZuXE8VvjCAVebDg5vTYhQ33jUwGgbn1wFmytUMgMmvK8A8Gpshl0sO2GBIZoh6_pwLrk657ZEtv-hx9fYKnzwyrfHqxtSswMAyr7XtKl8Ha1I03uFMSaYaaBTwVXCHByhzr4PVXfKAYJNbbcteUZfE8ODlBQkjQLI0IB78Nu8XIRrdzTF_5LCuM6rLUNtX6_KdzPpeX9KEtB7OBAfkdZEtBzGI-aYNLtIaL4qO6cVxBeVDLMoj9kVsRPylrwhEFQcGOjtJhwJwXFzTMZVhkiLFCHxZkkjoMrK5osSRlhduuGI9ot8XTUKQ\", \n" + + " \"use\": \"sig\"\n" + + " }\n" + + " ]\n" + + "}\n"; + + @Before + public void setup() { + this.server = new MockWebServer(); + this.source = new ReactiveRemoteJWKSource(this.server.url("/").toString()); + + this.server.enqueue(new MockResponse().setBody(this.keys)); + this.selector = new JWKSelector(this.matcher); + } + + @Test + public void getWhenMultipleRequestThenCached() { + when(this.matcher.matches(any())).thenReturn(true); + + this.source.get(this.selector).block(); + this.source.get(this.selector).block(); + + assertThat(this.server.getRequestCount()).isEqualTo(1); + } + + @Test + public void getWhenMatchThenCreatesKeys() { + when(this.matcher.matches(any())).thenReturn(true); + + List keys = this.source.get(this.selector).block(); + assertThat(keys).hasSize(2); + JWK key1 = keys.get(0); + assertThat(key1.getKeyID()).isEqualTo("1923397381d9574bb873202a90c32b7ceeaed027"); + assertThat(key1.getAlgorithm().getName()).isEqualTo("RS256"); + assertThat(key1.getKeyType()).isEqualTo(KeyType.RSA); + assertThat(key1.getKeyUse()).isEqualTo(KeyUse.SIGNATURE); + + JWK key2 = keys.get(1); + assertThat(key2.getKeyID()).isEqualTo("7ddf54d3032d1f0d48c3618892ca74c1ac30ad77"); + assertThat(key2.getAlgorithm().getName()).isEqualTo("RS256"); + assertThat(key2.getKeyType()).isEqualTo(KeyType.RSA); + assertThat(key2.getKeyUse()).isEqualTo(KeyUse.SIGNATURE); + } + + @Test + public void getWhenNoMatchAndNoKeyIdThenEmpty() { + when(this.matcher.matches(any())).thenReturn(false); + when(this.matcher.getKeyIDs()).thenReturn(Collections.emptySet()); + + assertThat(this.source.get(this.selector).block()).isEmpty(); + } + + @Test + public void getWhenNoMatchAndKeyIdNotMatchThenRefreshAndFoundThenFound() { + this.server.enqueue(new MockResponse().setBody(this.keys2)); + when(this.matcher.matches(any())).thenReturn(false, false, true); + when(this.matcher.getKeyIDs()).thenReturn(Collections.singleton("rotated")); + + List keys = this.source.get(this.selector).block(); + + assertThat(keys).hasSize(1); + assertThat(keys.get(0).getKeyID()).isEqualTo("rotated"); + } + + @Test + public void getWhenNoMatchAndKeyIdNotMatchThenRefreshAndNotFoundThenEmpty() { + this.server.enqueue(new MockResponse().setBody(this.keys2)); + when(this.matcher.matches(any())).thenReturn(false, false, false); + when(this.matcher.getKeyIDs()).thenReturn(Collections.singleton("rotated")); + + List keys = this.source.get(this.selector).block(); + + assertThat(keys).isEmpty(); + } + + @Test + public void getWhenNoMatchAndKeyIdMatchThenEmpty() { + when(this.matcher.matches(any())).thenReturn(false); + when(this.matcher.getKeyIDs()).thenReturn(Collections.singleton("7ddf54d3032d1f0d48c3618892ca74c1ac30ad77")); + + assertThat(this.source.get(this.selector).block()).isEmpty(); + } +} diff --git a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle new file mode 100644 index 00000000000..7fac327e252 --- /dev/null +++ b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle @@ -0,0 +1,19 @@ +apply plugin: 'io.spring.convention.spring-module' + +dependencies { + compile project(':spring-security-core') + compile project(':spring-security-oauth2-core') + compile project(':spring-security-web') + compile springCoreDependency + + optional project(':spring-security-oauth2-jose') + optional 'io.projectreactor:reactor-core' + optional 'org.springframework:spring-webflux' + + provided 'javax.servlet:javax.servlet-api' + + testCompile 'com.squareup.okhttp3:mockwebserver' + testCompile 'com.fasterxml.jackson.core:jackson-databind' + testCompile 'io.projectreactor.netty:reactor-netty' + testCompile 'io.projectreactor:reactor-test' +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationToken.java new file mode 100644 index 00000000000..ed84ab6347e --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationToken.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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; + +import java.util.Collections; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; +import org.springframework.util.Assert; + +/** + * An {@link Authentication} that contains a + * Bearer Token. + * + * Used by {@link BearerTokenAuthenticationFilter} to prepare an authentication attempt and supported + * by {@link JwtAuthenticationProvider}. + * + * @author Josh Cummings + * @since 5.1 + */ +public class BearerTokenAuthenticationToken extends AbstractAuthenticationToken { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + private String token; + + /** + * Create a {@code BearerTokenAuthenticationToken} using the provided parameter(s) + * + * @param token - the bearer token + */ + public BearerTokenAuthenticationToken(String token) { + super(Collections.emptyList()); + + Assert.hasText(token, "token cannot be empty"); + + this.token = token; + } + + /** + * Get the Bearer Token + * @return the token that proves the caller's authority to perform the {@link javax.servlet.http.HttpServletRequest} + */ + public String getToken() { + return this.token; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getCredentials() { + return this.getToken(); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getPrincipal() { + return this.getToken(); + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java new file mode 100644 index 00000000000..7b3abbaeee0 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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; + +import org.springframework.http.HttpStatus; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.util.Assert; + +/** + * A representation of a Bearer Token Error. + * + * @author Vedran Pavic + * @author Josh Cummings + * @since 5.1 + * @see BearerTokenErrorCodes + * @see RFC 6750 Section 3: The WWW-Authenticate + * Response Header Field + */ +public final class BearerTokenError extends OAuth2Error { + + private final HttpStatus httpStatus; + + private final String scope; + + /** + * Create a {@code BearerTokenError} using the provided parameters + * + * @param errorCode the error code + * @param httpStatus the HTTP status + */ + public BearerTokenError(String errorCode, HttpStatus httpStatus, String description, String errorUri) { + this(errorCode, httpStatus, description, errorUri, null); + } + + /** + * Create a {@code BearerTokenError} using the provided parameters + * + * @param errorCode the error code + * @param httpStatus the HTTP status + * @param description the description + * @param errorUri the URI + * @param scope the scope + */ + public BearerTokenError(String errorCode, HttpStatus httpStatus, String description, String errorUri, String scope) { + super(errorCode, description, errorUri); + Assert.notNull(httpStatus, "httpStatus cannot be null"); + + Assert.isTrue(isDescriptionValid(description), + "description contains invalid ASCII characters, it must conform to RFC 6750"); + Assert.isTrue(isErrorCodeValid(errorCode), + "errorCode contains invalid ASCII characters, it must conform to RFC 6750"); + Assert.isTrue(isErrorUriValid(errorUri), + "errorUri contains invalid ASCII characters, it must conform to RFC 6750"); + Assert.isTrue(isScopeValid(scope), + "scope contains invalid ASCII characters, it must conform to RFC 6750"); + + this.httpStatus = httpStatus; + this.scope = scope; + } + + /** + * Return the HTTP status. + * @return the HTTP status + */ + public HttpStatus getHttpStatus() { + return this.httpStatus; + } + + /** + * Return the scope. + * @return the scope + */ + public String getScope() { + return this.scope; + } + + private static boolean isDescriptionValid(String description) { + return description == null || + description.chars().allMatch(c -> + withinTheRangeOf(c, 0x20, 0x21) || + withinTheRangeOf(c, 0x23, 0x5B) || + withinTheRangeOf(c, 0x5D, 0x7E)); + } + + private static boolean isErrorCodeValid(String errorCode) { + return errorCode.chars().allMatch(c -> + withinTheRangeOf(c, 0x20, 0x21) || + withinTheRangeOf(c, 0x23, 0x5B) || + withinTheRangeOf(c, 0x5D, 0x7E)); + } + + private static boolean isErrorUriValid(String errorUri) { + return errorUri == null || + errorUri.chars().allMatch(c -> + c == 0x21 || + withinTheRangeOf(c, 0x23, 0x5B) || + withinTheRangeOf(c, 0x5D, 0x7E)); + } + + private static boolean isScopeValid(String scope) { + return scope == null || + scope.chars().allMatch(c -> + withinTheRangeOf(c, 0x20, 0x21) || + withinTheRangeOf(c, 0x23, 0x5B) || + withinTheRangeOf(c, 0x5D, 0x7E)); + } + + private static boolean withinTheRangeOf(int c, int min, int max) { + return c >= min && c <= max; + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorCodes.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorCodes.java new file mode 100644 index 00000000000..06cb884868b --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorCodes.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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; + +/** + * Standard error codes defined by the OAuth 2.0 Authorization Framework: Bearer Token Usage. + * + * @author Vedran Pavic + * @since 5.1 + * @see RFC 6750 Section 3.1: Error Codes + */ +public interface BearerTokenErrorCodes { + + /** + * {@code invalid_request} - The request is missing a required parameter, includes an unsupported parameter or + * parameter value, repeats the same parameter, uses more than one method for including an access token, or is + * otherwise malformed. + */ + String INVALID_REQUEST = "invalid_request"; + + /** + * {@code invalid_token} - The access token provided is expired, revoked, malformed, or invalid for other + * reasons. + */ + String INVALID_TOKEN = "invalid_token"; + + /** + * {@code insufficient_scope} - The request requires higher privileges than provided by the access token. + */ + String INSUFFICIENT_SCOPE = "insufficient_scope"; + +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java new file mode 100644 index 00000000000..896ac031d86 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.Map; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.Assert; + +/** + * Base class for {@link AbstractAuthenticationToken} implementations + * that expose common attributes between different OAuth 2.0 Access Token Formats. + * + *

      + * For example, a {@link Jwt} could expose its {@link Jwt#getClaims() claims} via + * {@link #getTokenAttributes()} or an "Introspected" OAuth 2.0 Access Token + * could expose the attributes of the Introspection Response via {@link #getTokenAttributes()}. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AccessToken + * @see Jwt + * @see 2.2 Introspection Response + */ +public abstract class AbstractOAuth2TokenAuthenticationToken extends AbstractAuthenticationToken { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + private T token; + + /** + * Sub-class constructor. + */ + protected AbstractOAuth2TokenAuthenticationToken(T token) { + + this(token, null); + } + + /** + * Sub-class constructor. + * + * @param authorities the authorities assigned to the Access Token + */ + protected AbstractOAuth2TokenAuthenticationToken( + T token, + Collection authorities) { + + super(authorities); + + Assert.notNull(token, "token cannot be null"); + this.token = token; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getPrincipal() { + return this.getToken(); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getCredentials() { + return this.getToken(); + } + + /** + * Get the token bound to this {@link Authentication}. + */ + public final T getToken() { + return this.token; + } + + /** + * Returns the attributes of the access token. + * + * @return a {@code Map} of the attributes in the access token. + */ + public abstract Map getTokenAttributes(); +} 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 new file mode 100644 index 00000000000..fafb1114225 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.util.Assert; + +/** + * An {@link AuthenticationProvider} implementation of the {@link Jwt}-encoded + * Bearer Tokens + * for protecting OAuth 2.0 Resource Servers. + *

      + *

      + * This {@link AuthenticationProvider} is responsible for decoding and verifying a {@link Jwt}-encoded access token, + * returning its claims set as part of the {@see Authentication} statement. + *

      + *

      + * Scopes are translated into {@link GrantedAuthority}s according to the following algorithm: + * + * 1. If there is a "scope" or "scp" attribute, then + * if a {@link String}, then split by spaces and return, or + * if a {@link Collection}, then simply return + * 2. Take the resulting {@link Collection} of {@link String}s and prepend the "SCOPE_" keyword, adding + * as {@link GrantedAuthority}s. + * + * @author Josh Cummings + * @author Joe Grandja + * @since 5.1 + * @see AuthenticationProvider + * @see JwtDecoder + */ +public final class JwtAuthenticationProvider implements AuthenticationProvider { + private final JwtDecoder jwtDecoder; + + private final JwtConverter jwtConverter = new JwtConverter(); + + private static final OAuth2Error DEFAULT_INVALID_TOKEN = + invalidToken("An error occurred while attempting to decode the Jwt: Invalid token"); + + public JwtAuthenticationProvider(JwtDecoder jwtDecoder) { + Assert.notNull(jwtDecoder, "jwtDecoder cannot be null"); + + this.jwtDecoder = jwtDecoder; + } + + /** + * Decode and validate the + * Bearer Token. + * + * @param authentication the authentication request object. + * + * @return A successful authentication + * @throws AuthenticationException if authentication failed for some reason + */ + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication; + + Jwt jwt; + try { + jwt = this.jwtDecoder.decode(bearer.getToken()); + } catch (JwtException failed) { + OAuth2Error invalidToken = invalidToken(failed.getMessage()); + throw new OAuth2AuthenticationException(invalidToken, invalidToken.getDescription(), failed); + } + + JwtAuthenticationToken token = this.jwtConverter.convert(jwt); + token.setDetails(bearer.getDetails()); + + return token; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supports(Class authentication) { + return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication); + } + + private static OAuth2Error invalidToken(String message) { + try { + return new BearerTokenError( + BearerTokenErrorCodes.INVALID_TOKEN, + HttpStatus.UNAUTHORIZED, + message, + "https://tools.ietf.org/html/rfc6750#section-3.1"); + } catch (IllegalArgumentException malformed) { + // some third-party library error messages are not suitable for RFC 6750's error message charset + return DEFAULT_INVALID_TOKEN; + } + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java new file mode 100644 index 00000000000..85ee5ed2a8f --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.Map; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.core.Transient; +import org.springframework.security.oauth2.jwt.Jwt; + +/** + * An implementation of an {@link AbstractOAuth2TokenAuthenticationToken} + * representing a {@link Jwt} {@code Authentication}. + * + * @author Joe Grandja + * @since 5.1 + * @see AbstractOAuth2TokenAuthenticationToken + * @see Jwt + */ +@Transient +public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + /** + * Constructs a {@code JwtAuthenticationToken} using the provided parameters. + * + * @param jwt the JWT + */ + public JwtAuthenticationToken(Jwt jwt) { + super(jwt); + } + + /** + * Constructs a {@code JwtAuthenticationToken} using the provided parameters. + * + * @param jwt the JWT + * @param authorities the authorities assigned to the JWT + */ + public JwtAuthenticationToken(Jwt jwt, Collection authorities) { + super(jwt, authorities); + this.setAuthenticated(true); + } + + /** + * {@inheritDoc} + */ + @Override + public Map getTokenAttributes() { + return this.getToken().getClaims(); + } + + /** + * The {@link Jwt}'s subject, if any + */ + @Override + public String getName() { + return this.getToken().getSubject(); + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtConverter.java new file mode 100644 index 00000000000..0ade72982fa --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtConverter.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.StringUtils; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @author Rob Winch + * @since 5.1 + */ +class JwtConverter { + private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_"; + + private static final Collection WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES = + Arrays.asList("scope", "scp"); + + + JwtAuthenticationToken convert(Jwt jwt) { + Collection authorities = + this.getScopes(jwt) + .stream() + .map(authority -> SCOPE_AUTHORITY_PREFIX + authority) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + return new JwtAuthenticationToken(jwt, authorities); + } + + private Collection getScopes(Jwt jwt) { + for ( String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES ) { + Object scopes = jwt.getClaims().get(attributeName); + if (scopes instanceof String) { + if (StringUtils.hasText((String) scopes)) { + return Arrays.asList(((String) scopes).split(" ")); + } else { + return Collections.emptyList(); + } + } else if (scopes instanceof Collection) { + return (Collection) scopes; + } + } + + return Collections.emptyList(); + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManager.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManager.java new file mode 100644 index 00000000000..84b40cbe170 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManager.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 org.springframework.http.HttpStatus; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; + +/** + * A {@link ReactiveAuthenticationManager} for Jwt tokens. + * + * @author Rob Winch + * @since 5.1 + */ +public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager { + private final JwtConverter jwtConverter = new JwtConverter(); + + private final ReactiveJwtDecoder jwtDecoder; + + public JwtReactiveAuthenticationManager(ReactiveJwtDecoder jwtDecoder) { + Assert.notNull(jwtDecoder, "jwtDecoder cannot be null"); + this.jwtDecoder = jwtDecoder; + } + + @Override + public Mono authenticate(Authentication authentication) { + return Mono.justOrEmpty(authentication) + .filter(a -> a instanceof BearerTokenAuthenticationToken) + .cast(BearerTokenAuthenticationToken.class) + .map(BearerTokenAuthenticationToken::getToken) + .flatMap(this.jwtDecoder::decode) + .map(this.jwtConverter::convert) + .cast(Authentication.class) + .onErrorMap(JwtException.class, this::onError); + } + + private OAuth2AuthenticationException onError(JwtException e) { + OAuth2Error invalidRequest = invalidToken(e.getMessage()); + return new OAuth2AuthenticationException(invalidRequest, e.getMessage()); + } + + private static OAuth2Error invalidToken(String message) { + return new BearerTokenError( + BearerTokenErrorCodes.INVALID_TOKEN, + HttpStatus.UNAUTHORIZED, + message, + "https://tools.ietf.org/html/rfc6750#section-3.1"); + } +} diff --git a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java similarity index 70% rename from samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java rename to oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java index 6cefe5bb02b..0b0a403f624 100644 --- a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/package-info.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 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. @@ -15,6 +15,6 @@ */ /** - * Auto-configuration for Spring Security's Reactive OAuth 2 client. This will be merged into Spring Boot 2.1. + * OAuth 2.0 Resource Server {@code Authentication}s and supporting classes and interfaces. */ -package org.springframework.boot.autoconfigure.security.oauth2.client; +package org.springframework.security.oauth2.server.resource.authentication; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java new file mode 100644 index 00000000000..43e164f9786 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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. + */ + +/** + * OAuth 2.0 Resource Server core classes and interfaces providing support. + */ +package org.springframework.security.oauth2.server.resource; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java new file mode 100644 index 00000000000..ce6e4214d06 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +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.server.resource.BearerTokenError; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.util.StringUtils; + +/** + * An {@link AuthenticationEntryPoint} implementation used to commence authentication of protected resource requests + * using {@link BearerTokenAuthenticationFilter}. + *

      + * Uses information provided by {@link BearerTokenError} to set HTTP response status code and populate + * {@code WWW-Authenticate} HTTP header. + * + * @author Vedran Pavic + * @since 5.1 + * @see BearerTokenError + * @see RFC 6750 Section 3: The WWW-Authenticate + * Response Header Field + */ +public final class BearerTokenAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private String realmName; + + /** + * Collect error details from the provided parameters and format according to + * RFC 6750, specifically {@code error}, {@code error_description}, {@code error_uri}, and {@scope scope}. + * + * @param request that resulted in an AuthenticationException + * @param response so that the user agent can begin authentication + * @param authException that caused the invocation + */ + @Override + public void commence( + HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) + throws IOException, ServletException { + + HttpStatus status = HttpStatus.UNAUTHORIZED; + + Map parameters = new LinkedHashMap<>(); + + if (this.realmName != null) { + parameters.put("realm", this.realmName); + } + + if (authException instanceof OAuth2AuthenticationException) { + OAuth2Error error = ((OAuth2AuthenticationException) authException).getError(); + + parameters.put("error", error.getErrorCode()); + + if (StringUtils.hasText(error.getDescription())) { + parameters.put("error_description", error.getDescription()); + } + + if (StringUtils.hasText(error.getUri())) { + parameters.put("error_uri", error.getUri()); + } + + if (error instanceof BearerTokenError) { + BearerTokenError bearerTokenError = (BearerTokenError) error; + + if (StringUtils.hasText(bearerTokenError.getScope())) { + parameters.put("scope", bearerTokenError.getScope()); + } + + status = ((BearerTokenError) error).getHttpStatus(); + } + } + + String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters); + + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate); + response.setStatus(status.value()); + } + + /** + * Set the default realm name to use in the bearer token error response + * + * @param realmName + */ + public final void setRealmName(String realmName) { + this.realmName = realmName; + } + + private static String computeWWWAuthenticateHeaderValue(Map parameters) { + String wwwAuthenticate = "Bearer"; + if (!parameters.isEmpty()) { + wwwAuthenticate += parameters.entrySet().stream() + .map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"") + .collect(Collectors.joining(", ", " ", "")); + } + + return wwwAuthenticate; + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java new file mode 100644 index 00000000000..5137d1d1a1b --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilter.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.authentication.AuthenticationManager; +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.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Authenticates requests that contain an OAuth 2.0 + * Bearer Token. + * + * This filter should be wired with an {@link AuthenticationManager} that can authenticate a + * {@link BearerTokenAuthenticationToken}. + * + * @author Josh Cummings + * @author Vedran Pavic + * @author Joe Grandja + * @since 5.1 + * @see The OAuth 2.0 Authorization Framework: Bearer Token Usage + * @see JwtAuthenticationProvider + */ +public final class BearerTokenAuthenticationFilter extends OncePerRequestFilter { + private final AuthenticationManager authenticationManager; + + private final AuthenticationDetailsSource authenticationDetailsSource = + new WebAuthenticationDetailsSource(); + + private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); + + private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint(); + + /** + * Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s) + * @param authenticationManager + */ + public BearerTokenAuthenticationFilter(AuthenticationManager authenticationManager) { + Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + this.authenticationManager = authenticationManager; + } + + /** + * Extract any Bearer Token from + * the request and attempt an authentication. + * + * @param request + * @param response + * @param filterChain + * @throws ServletException + * @throws IOException + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + final boolean debug = this.logger.isDebugEnabled(); + + String token; + + try { + token = this.bearerTokenResolver.resolve(request); + } catch ( OAuth2AuthenticationException invalid ) { + this.authenticationEntryPoint.commence(request, response, invalid); + return; + } + + if (token == null) { + filterChain.doFilter(request, response); + return; + } + + BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token); + + authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); + + try { + Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest); + + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authenticationResult); + SecurityContextHolder.setContext(context); + + filterChain.doFilter(request, response); + } catch (AuthenticationException failed) { + SecurityContextHolder.clearContext(); + + if (debug) { + this.logger.debug("Authentication request for failed: " + failed); + } + + this.authenticationEntryPoint.commence(request, response, failed); + } + } + + /** + * Set the {@link BearerTokenResolver} to use. Defaults to {@link DefaultBearerTokenResolver}. + * @param bearerTokenResolver the {@code BearerTokenResolver} to use + */ + public final void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) { + Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); + this.bearerTokenResolver = bearerTokenResolver; + } + + /** + * Set the {@link AuthenticationEntryPoint} to use. Defaults to {@link BearerTokenAuthenticationEntryPoint}. + * @param authenticationEntryPoint the {@code AuthenticationEntryPoint} to use + */ + public final void setAuthenticationEntryPoint(final AuthenticationEntryPoint authenticationEntryPoint) { + Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null"); + this.authenticationEntryPoint = authenticationEntryPoint; + } + +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java new file mode 100644 index 00000000000..b73be65ee38 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; + +/** + * A strategy for resolving Bearer Tokens + * from the {@link HttpServletRequest}. + * + * @author Vedran Pavic + * @since 5.1 + * @see RFC 6750 Section 2: Authenticated Requests + */ +public interface BearerTokenResolver { + + /** + * Resolve any Bearer Token + * value from the request. + * + * @param request the request + * @return the Bearer Token value or {@code null} if none found + * @throws OAuth2AuthenticationException if the found token is invalid + */ + String resolve(HttpServletRequest request); + +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java new file mode 100644 index 00000000000..c4532fa0297 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.util.StringUtils; + +/** + * The default {@link BearerTokenResolver} implementation based on RFC 6750. + * + * @author Vedran Pavic + * @since 5.1 + * @see RFC 6750 Section 2: Authenticated Requests + */ +public final class DefaultBearerTokenResolver implements BearerTokenResolver { + + private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?[a-zA-Z0-9-._~+/]+)=*$"); + + private boolean allowFormEncodedBodyParameter = false; + + private boolean allowUriQueryParameter = false; + + /** + * {@inheritDoc} + */ + @Override + public String resolve(HttpServletRequest request) { + String authorizationHeaderToken = resolveFromAuthorizationHeader(request); + String parameterToken = resolveFromRequestParameters(request); + if (authorizationHeaderToken != null) { + if (parameterToken != null) { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, + "Found multiple bearer tokens in the request", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + throw new OAuth2AuthenticationException(error); + } + return authorizationHeaderToken; + } + else if (parameterToken != null && isParameterTokenSupportedForRequest(request)) { + return parameterToken; + } + return null; + } + + /** + * Set if transport of access token using form-encoded body parameter is supported. Defaults to {@code false}. + * @param allowFormEncodedBodyParameter if the form-encoded body parameter is supported + */ + public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) { + this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter; + } + + /** + * Set if transport of access token using URI query parameter is supported. Defaults to {@code false}. + * + * The spec recommends against using this mechanism for sending bearer tokens, and even goes as far as + * stating that it was only included for completeness. + * + * @param allowUriQueryParameter if the URI query parameter is supported + */ + public void setAllowUriQueryParameter(boolean allowUriQueryParameter) { + this.allowUriQueryParameter = allowUriQueryParameter; + } + + private static String resolveFromAuthorizationHeader(HttpServletRequest request) { + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer")) { + Matcher matcher = authorizationPattern.matcher(authorization); + + if (!matcher.matches()) { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, + HttpStatus.UNAUTHORIZED, + "Bearer token is malformed", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + throw new OAuth2AuthenticationException(error); + } + + return matcher.group("token"); + } + return null; + } + + private static String resolveFromRequestParameters(HttpServletRequest request) { + String[] values = request.getParameterValues("access_token"); + if (values == null || values.length == 0) { + return null; + } + + if (values.length == 1) { + return values[0]; + } + + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, + "Found multiple bearer tokens in the request", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + throw new OAuth2AuthenticationException(error); + } + + private boolean isParameterTokenSupportedForRequest(HttpServletRequest request) { + return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod())) + || (this.allowUriQueryParameter && "GET".equals(request.getMethod()))); + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java new file mode 100644 index 00000000000..fa3212d3734 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.access; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.util.StringUtils; + +/** + * Translates any {@link AccessDeniedException} into an HTTP response in accordance with + * RFC 6750 Section 3: The WWW-Authenticate. + * + * So long as the class can prove that the request has a valid OAuth 2.0 {@link Authentication}, then will return an + * insufficient scope error; otherwise, + * it will simply indicate the scheme (Bearer) and any configured realm. + * + * @author Josh Cummings + * @since 5.1 + */ +public final class BearerTokenAccessDeniedHandler implements AccessDeniedHandler { + + private static final Collection WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES = + Arrays.asList("scope", "scp"); + + private String realmName; + + /** + * Collect error details from the provided parameters and format according to + * RFC 6750, specifically {@code error}, {@code error_description}, {@code error_uri}, and {@scope scope}. + * + * @param request that resulted in an AccessDeniedException + * @param response so that the user agent can be advised of the failure + * @param accessDeniedException that caused the invocation + * + */ + @Override + public void handle( + HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) + throws IOException, ServletException { + + Map parameters = new LinkedHashMap<>(); + + if (this.realmName != null) { + parameters.put("realm", this.realmName); + } + + if (request.getUserPrincipal() instanceof AbstractOAuth2TokenAuthenticationToken) { + AbstractOAuth2TokenAuthenticationToken token = + (AbstractOAuth2TokenAuthenticationToken) request.getUserPrincipal(); + + String scope = getScope(token); + + parameters.put("error", BearerTokenErrorCodes.INSUFFICIENT_SCOPE); + parameters.put("error_description", + String.format("The token provided has insufficient scope [%s] for this request", scope)); + parameters.put("error_uri", "https://tools.ietf.org/html/rfc6750#section-3.1"); + + if (StringUtils.hasText(scope)) { + parameters.put("scope", scope); + } + } + + String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters); + + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate); + response.setStatus(HttpStatus.FORBIDDEN.value()); + } + + /** + * Set the default realm name to use in the bearer token error response + * + * @param realmName + */ + public final void setRealmName(String realmName) { + this.realmName = realmName; + } + + private static String getScope(AbstractOAuth2TokenAuthenticationToken token) { + + Map attributes = token.getTokenAttributes(); + + for (String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES) { + Object scopes = attributes.get(attributeName); + if (scopes instanceof String) { + return (String) scopes; + } else if (scopes instanceof Collection) { + Collection coll = (Collection) scopes; + return (String) coll.stream() + .map(String::valueOf) + .collect(Collectors.joining(" ")); + } + } + + return ""; + } + + private static String computeWWWAuthenticateHeaderValue(Map parameters) { + String wwwAuthenticate = "Bearer"; + if (!parameters.isEmpty()) { + wwwAuthenticate += parameters.entrySet().stream() + .map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"") + .collect(Collectors.joining(", ", " ", "")); + } + + return wwwAuthenticate; + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java new file mode 100644 index 00000000000..948c0ca58f6 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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. + */ + +/** + * OAuth 2.0 Resource Server access denial classes and interfaces. + */ +package org.springframework.security.oauth2.server.resource.web.access; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java new file mode 100644 index 00000000000..5391fba2f42 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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. + */ + +/** + * OAuth 2.0 Resource Server {@code Filter}'s and supporting classes and interfaces. + */ +package org.springframework.security.oauth2.server.resource.web; 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 new file mode 100644 index 00000000000..d266e55efc6 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.server; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +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.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.server.ServerAuthenticationEntryPoint; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * An {@link AuthenticationEntryPoint} implementation used to commence authentication of protected resource requests + * using {@link BearerTokenAuthenticationFilter}. + *

      + * Uses information provided by {@link BearerTokenError} to set HTTP response status code and populate + * {@code WWW-Authenticate} HTTP header. + * + * @author Rob Winch + * @since 5.1 + * @see BearerTokenError + * @see RFC 6750 Section 3: The WWW-Authenticate + * Response Header Field + */ +public final class BearerTokenServerAuthenticationEntryPoint implements + ServerAuthenticationEntryPoint { + + private String realmName; + + public void setRealmName(String realmName) { + this.realmName = realmName; + } + + @Override + public Mono commence(ServerWebExchange exchange, AuthenticationException authException) { + HttpStatus status = getStatus(authException); + + Map parameters = createParameters(authException); + String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters); + ServerHttpResponse response = exchange.getResponse(); + response.getHeaders().set(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate); + response.setStatusCode(status); + return response.setComplete(); + } + + private Map createParameters(AuthenticationException authException) { + Map parameters = new LinkedHashMap<>(); + if (this.realmName != null) { + parameters.put("realm", this.realmName); + } + + if (authException instanceof OAuth2AuthenticationException) { + OAuth2Error error = ((OAuth2AuthenticationException) authException).getError(); + + parameters.put("error", error.getErrorCode()); + + if (StringUtils.hasText(error.getDescription())) { + parameters.put("error_description", error.getDescription()); + } + + if (StringUtils.hasText(error.getUri())) { + parameters.put("error_uri", error.getUri()); + } + + if (error instanceof BearerTokenError) { + BearerTokenError bearerTokenError = (BearerTokenError) error; + + if (StringUtils.hasText(bearerTokenError.getScope())) { + parameters.put("scope", bearerTokenError.getScope()); + } + } + } + return parameters; + } + + private HttpStatus getStatus(AuthenticationException authException) { + if (authException instanceof OAuth2AuthenticationException) { + OAuth2Error error = ((OAuth2AuthenticationException) authException).getError(); + if (error instanceof BearerTokenError) { + return ((BearerTokenError) error).getHttpStatus(); + } + } + return HttpStatus.UNAUTHORIZED; + } + + private static String computeWWWAuthenticateHeaderValue(Map parameters) { + String wwwAuthenticate = "Bearer"; + if (!parameters.isEmpty()) { + wwwAuthenticate += parameters.entrySet().stream() + .map(attribute -> attribute.getKey() + "=\"" + attribute.getValue() + "\"") + .collect(Collectors.joining(", ", " ", "")); + } + + return wwwAuthenticate; + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java new file mode 100644 index 00000000000..1025dfb9c33 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.server; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A strategy for resolving Bearer Tokens + * from the {@link ServerWebExchange}. + * + * @author Rob Winch + * @since 5.1 + * @see RFC 6750 Section 2: Authenticated Requests + */ +public class ServerBearerTokenAuthenticationConverter implements + Function> { + private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?[a-zA-Z0-9-._~+/]+)=*$"); + + private boolean allowUriQueryParameter = false; + + public Mono apply(ServerWebExchange exchange) { + return Mono.justOrEmpty(this.token(exchange.getRequest())) + .map(BearerTokenAuthenticationToken::new); + } + + private String token(ServerHttpRequest request) { + String authorizationHeaderToken = resolveFromAuthorizationHeader(request.getHeaders()); + String parameterToken = request.getQueryParams().getFirst("access_token"); + if (authorizationHeaderToken != null) { + if (parameterToken != null) { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, + "Found multiple bearer tokens in the request", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + throw new OAuth2AuthenticationException(error); + } + return authorizationHeaderToken; + } + else if (parameterToken != null && isParameterTokenSupportedForRequest(request)) { + return parameterToken; + } + return null; + } + + /** + * Set if transport of access token using URI query parameter is supported. Defaults to {@code false}. + * + * The spec recommends against using this mechanism for sending bearer tokens, and even goes as far as + * stating that it was only included for completeness. + * + * @param allowUriQueryParameter if the URI query parameter is supported + */ + public void setAllowUriQueryParameter(boolean allowUriQueryParameter) { + this.allowUriQueryParameter = allowUriQueryParameter; + } + + private static String resolveFromAuthorizationHeader(HttpHeaders headers) { + String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION); + if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer")) { + Matcher matcher = authorizationPattern.matcher(authorization); + + if ( !matcher.matches() ) { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, + HttpStatus.BAD_REQUEST, + "Bearer token is malformed", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + throw new OAuth2AuthenticationException(error); + } + + return matcher.group("token"); + } + return null; + } + + private boolean isParameterTokenSupportedForRequest(ServerHttpRequest request) { + return this.allowUriQueryParameter && HttpMethod.GET.equals(request.getMethod()); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationTokenTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationTokenTests.java new file mode 100644 index 00000000000..cb5fcc426bc --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenAuthenticationTokenTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link BearerTokenAuthenticationToken} + * + * @author Josh Cummings + */ +public class BearerTokenAuthenticationTokenTests { + @Test + public void constructorWhenTokenIsNullThenThrowsException() { + assertThatCode(() -> new BearerTokenAuthenticationToken(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("token cannot be empty"); + } + + @Test + public void constructorWhenTokenIsEmptyThenThrowsException() { + assertThatCode(() -> new BearerTokenAuthenticationToken("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("token cannot be empty"); + } + + @Test + public void constructorWhenTokenHasValueThenConstructedCorrectly() { + BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token"); + + assertThat(token.getToken()).isEqualTo("token"); + assertThat(token.getPrincipal()).isEqualTo("token"); + assertThat(token.getCredentials()).isEqualTo("token"); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorTests.java new file mode 100644 index 00000000000..6ac6b651fc8 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/BearerTokenErrorTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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; + +import org.junit.Test; + +import org.springframework.http.HttpStatus; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link BearerTokenError} + * + * @author Vedran Pavic + * @author Josh Cummings + */ +public class BearerTokenErrorTests { + + private static final String TEST_ERROR_CODE = "test-code"; + + private static final HttpStatus TEST_HTTP_STATUS = HttpStatus.UNAUTHORIZED; + + private static final String TEST_DESCRIPTION = "test-description"; + + private static final String TEST_URI = "http://example.com"; + + private static final String TEST_SCOPE = "test-scope"; + + @Test + public void constructorWithErrorCodeWhenErrorCodeIsValidThenCreated() { + BearerTokenError error = new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, null, null); + + assertThat(error.getErrorCode()).isEqualTo(TEST_ERROR_CODE); + assertThat(error.getHttpStatus()).isEqualTo(TEST_HTTP_STATUS); + assertThat(error.getDescription()).isNull(); + assertThat(error.getUri()).isNull(); + assertThat(error.getScope()).isNull(); + } + + @Test + public void constructorWithErrorCodeAndHttpStatusWhenErrorCodeIsNullThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(null, TEST_HTTP_STATUS, null, null)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty"); + } + + @Test + public void constructorWithErrorCodeAndHttpStatusWhenErrorCodeIsEmptyThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError("", TEST_HTTP_STATUS, null, null)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty"); + } + + @Test + public void constructorWithErrorCodeAndHttpStatusWhenHttpStatusIsNullThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, null, null, null)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("httpStatus cannot be null"); + } + + @Test + public void constructorWithAllParametersWhenAllParametersAreValidThenCreated() { + BearerTokenError error = new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI, + TEST_SCOPE); + + assertThat(error.getErrorCode()).isEqualTo(TEST_ERROR_CODE); + assertThat(error.getHttpStatus()).isEqualTo(TEST_HTTP_STATUS); + assertThat(error.getDescription()).isEqualTo(TEST_DESCRIPTION); + assertThat(error.getUri()).isEqualTo(TEST_URI); + assertThat(error.getScope()).isEqualTo(TEST_SCOPE); + } + + @Test + public void constructorWithAllParametersWhenErrorCodeIsNullThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(null, TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty"); + } + + @Test + public void constructorWithAllParametersWhenErrorCodeIsEmptyThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError("", TEST_HTTP_STATUS, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("errorCode cannot be empty"); + } + + @Test + public void constructorWithAllParametersWhenHttpStatusIsNullThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, null, TEST_DESCRIPTION, TEST_URI, TEST_SCOPE)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("httpStatus cannot be null"); + } + + @Test + public void constructorWithAllParametersWhenErrorCodeIsInvalidThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE + "\"", TEST_HTTP_STATUS, TEST_DESCRIPTION, + TEST_URI, TEST_SCOPE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("errorCode") + .hasMessageContaining("RFC 6750"); + } + + @Test + public void constructorWithAllParametersWhenDescriptionIsInvalidThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION + "\"", + TEST_URI, TEST_SCOPE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("description") + .hasMessageContaining("RFC 6750"); + } + + @Test + public void constructorWithAllParametersWhenErrorUriIsInvalidThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION, + TEST_URI + "\"", TEST_SCOPE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("errorUri") + .hasMessageContaining("RFC 6750"); + } + + @Test + public void constructorWithAllParametersWhenScopeIsInvalidThenThrowIllegalArgumentException() { + assertThatCode(() -> new BearerTokenError(TEST_ERROR_CODE, TEST_HTTP_STATUS, TEST_DESCRIPTION, + TEST_URI, TEST_SCOPE + "\"")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("scope") + .hasMessageContaining("RFC 6750"); + } +} 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 new file mode 100644 index 00000000000..51e898a5eae --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProviderTests.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.time.Instant; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; + +import org.assertj.core.util.Maps; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link JwtAuthenticationProvider} + * + * @author Josh Cummings + */ +@RunWith(MockitoJUnitRunner.class) +public class JwtAuthenticationProviderTests { + @Mock + JwtDecoder jwtDecoder; + + JwtAuthenticationProvider provider; + + @Before + public void setup() { + this.provider = + new JwtAuthenticationProvider(this.jwtDecoder); + } + + @Test + public void authenticateWhenJwtDecodesThenAuthenticationHasAttributesContainedInJwt() { + BearerTokenAuthenticationToken token = this.authentication(); + + Map claims = new HashMap<>(); + claims.put("name", "value"); + Jwt jwt = this.jwt(claims); + + when(this.jwtDecoder.decode("token")).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + assertThat(authentication.getTokenAttributes()).isEqualTo(claims); + } + + @Test + public void authenticateWhenJwtDecodeFailsThenRespondsWithInvalidToken() { + BearerTokenAuthenticationToken token = this.authentication(); + + when(this.jwtDecoder.decode("token")).thenThrow(JwtException.class); + + assertThatCode(() -> this.provider.authenticate(token)) + .matches(failed -> failed instanceof OAuth2AuthenticationException) + .matches(errorCode(BearerTokenErrorCodes.INVALID_TOKEN)); + } + + @Test + public void authenticateWhenTokenHasScopeAttributeThenTranslatedToAuthorities() { + BearerTokenAuthenticationToken token = this.authentication(); + + Jwt jwt = this.jwt(Maps.newHashMap("scope", "message:read message:write")); + + when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + Collection authorities = authentication.getAuthorities(); + + assertThat(authorities).containsExactly( + new SimpleGrantedAuthority("SCOPE_message:read"), + new SimpleGrantedAuthority("SCOPE_message:write")); + } + + @Test + public void authenticateWhenTokenHasEmptyScopeAttributeThenTranslatedToNoAuthorities() { + BearerTokenAuthenticationToken token = this.authentication(); + + Jwt jwt = this.jwt(Maps.newHashMap("scope", "")); + + when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + Collection authorities = authentication.getAuthorities(); + + assertThat(authorities).containsExactly(); + } + + @Test + public void authenticateWhenTokenHasScpAttributeThenTranslatedToAuthorities() { + BearerTokenAuthenticationToken token = this.authentication(); + + Jwt jwt = this.jwt(Maps.newHashMap("scp", Arrays.asList("message:read", "message:write"))); + + when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + Collection authorities = authentication.getAuthorities(); + + assertThat(authorities).containsExactly( + new SimpleGrantedAuthority("SCOPE_message:read"), + new SimpleGrantedAuthority("SCOPE_message:write")); + } + + @Test + public void authenticateWhenTokenHasEmptyScpAttributeThenTranslatedToNoAuthorities() { + BearerTokenAuthenticationToken token = this.authentication(); + + Jwt jwt = this.jwt(Maps.newHashMap("scp", Arrays.asList())); + + when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + Collection authorities = authentication.getAuthorities(); + + assertThat(authorities).containsExactly(); + } + + @Test + public void authenticateWhenTokenHasBothScopeAndScpThenScopeAttributeIsTranslatedToAuthorities() { + BearerTokenAuthenticationToken token = this.authentication(); + + Map claims = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write")); + claims.put("scope", "missive:read missive:write"); + Jwt jwt = this.jwt(claims); + + when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + Collection authorities = authentication.getAuthorities(); + + assertThat(authorities).containsExactly( + new SimpleGrantedAuthority("SCOPE_missive:read"), + new SimpleGrantedAuthority("SCOPE_missive:write")); + } + + @Test + public void authenticateWhenTokenHasEmptyScopeAndNonEmptyScpThenScopeAttributeIsTranslatedToNoAuthorities() { + BearerTokenAuthenticationToken token = this.authentication(); + + Map claims = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write")); + claims.put("scope", ""); + Jwt jwt = this.jwt(claims); + + when(this.jwtDecoder.decode(token.getToken())).thenReturn(jwt); + + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) this.provider.authenticate(token); + + Collection authorities = authentication.getAuthorities(); + + assertThat(authorities).containsExactly(); + } + + @Test + public void authenticateWhenDecoderThrowsIncompatibleErrorMessageThenWrapsWithGenericOne() { + BearerTokenAuthenticationToken token = this.authentication(); + + when(this.jwtDecoder.decode(token.getToken())).thenThrow(new JwtException("with \"invalid\" chars")); + + assertThatCode(() -> this.provider.authenticate(token)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasFieldOrPropertyWithValue( + "error.description", + "An error occurred while attempting to decode the Jwt: Invalid token"); + } + + @Test + public void supportsWhenBearerTokenAuthenticationTokenThenReturnsTrue() { + assertThat(this.provider.supports(BearerTokenAuthenticationToken.class)).isTrue(); + } + + private BearerTokenAuthenticationToken authentication() { + return new BearerTokenAuthenticationToken("token"); + } + + private Jwt jwt(Map claims) { + Map headers = new HashMap<>(); + headers.put("alg", JwsAlgorithms.RS256); + + return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims); + } + + private Predicate errorCode(String errorCode) { + return failed -> + ((OAuth2AuthenticationException) failed).getError().getErrorCode() == errorCode; + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java new file mode 100644 index 00000000000..d75a8c1629d --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.time.Instant; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.assertj.core.util.Maps; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link JwtAuthenticationToken} + * + * @author Josh Cummings + */ +@RunWith(MockitoJUnitRunner.class) +public class JwtAuthenticationTokenTests { + + @Test + public void getNameWhenJwtHasSubjectThenReturnsSubject() { + Jwt jwt = this.jwt(Maps.newHashMap("sub", "Carl")); + + JwtAuthenticationToken token = new JwtAuthenticationToken(jwt); + + assertThat(token.getName()).isEqualTo("Carl"); + } + + @Test + public void getNameWhenJwtHasNoSubjectThenReturnsNull() { + Jwt jwt = this.jwt(Maps.newHashMap("claim", "value")); + + JwtAuthenticationToken token = new JwtAuthenticationToken(jwt); + + assertThat(token.getName()).isNull(); + } + + @Test + public void constructorWhenJwtIsNullThenThrowsException() { + assertThatCode(() -> new JwtAuthenticationToken(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("token cannot be null"); + } + + @Test + public void constructorWhenUsingCorrectParametersThenConstructedCorrectly() { + Collection authorities = Arrays.asList(new SimpleGrantedAuthority("test")); + Map claims = Maps.newHashMap("claim", "value"); + Jwt jwt = this.jwt(claims); + + JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities); + + assertThat(token.getAuthorities()).isEqualTo(authorities); + assertThat(token.getPrincipal()).isEqualTo(jwt); + assertThat(token.getCredentials()).isEqualTo(jwt); + assertThat(token.getToken()).isEqualTo(jwt); + assertThat(token.getTokenAttributes()).isEqualTo(claims); + assertThat(token.isAuthenticated()).isTrue(); + } + + @Test + public void constructorWhenUsingOnlyJwtThenConstructedCorrectly() { + Map claims = Maps.newHashMap("claim", "value"); + Jwt jwt = this.jwt(claims); + + JwtAuthenticationToken token = new JwtAuthenticationToken(jwt); + + assertThat(token.getAuthorities()).isEmpty(); + assertThat(token.getPrincipal()).isEqualTo(jwt); + assertThat(token.getCredentials()).isEqualTo(jwt); + assertThat(token.getToken()).isEqualTo(jwt); + assertThat(token.getTokenAttributes()).isEqualTo(claims); + assertThat(token.isAuthenticated()).isFalse(); + } + + private Jwt jwt(Map claims) { + Map headers = new HashMap<>(); + headers.put("alg", JwsAlgorithms.RS256); + + return new Jwt("token", Instant.now(), Instant.now().plusSeconds(3600), headers, claims); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManagerTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManagerTests.java new file mode 100644 index 00000000000..ac4ca5b2981 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManagerTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class JwtReactiveAuthenticationManagerTests { + @Mock + private ReactiveJwtDecoder jwtDecoder; + + private JwtReactiveAuthenticationManager manager; + + private Jwt jwt; + + @Before + public void setup() { + this.manager = new JwtReactiveAuthenticationManager(this.jwtDecoder); + + Map claims = new HashMap<>(); + claims.put("scope", "message:read message:write"); + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.from(issuedAt).plusSeconds(3600); + this.jwt = new Jwt("jwt", issuedAt, expiresAt, claims, claims); + } + + @Test + public void constructorWhenJwtDecoderNullThenIllegalArgumentException() { + this.jwtDecoder = null; + assertThatCode(() -> new JwtReactiveAuthenticationManager(this.jwtDecoder)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void authenticateWhenWrongTypeThenEmpty() { + TestingAuthenticationToken token = new TestingAuthenticationToken("foo", "bar"); + + assertThat(this.manager.authenticate(token).block()).isNull(); + } + + @Test + public void authenticateWhenEmptyJwtThenEmpty() { + BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token-1"); + when(this.jwtDecoder.decode(token.getToken())).thenReturn(Mono.empty()); + + assertThat(this.manager.authenticate(token).block()).isNull(); + } + + @Test + public void authenticateWhenJwtExceptionThenOAuth2AuthenticationException() { + BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token-1"); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.error(new JwtException("Oops"))); + + assertThatCode(() -> this.manager.authenticate(token).block()) + .isInstanceOf(OAuth2AuthenticationException.class); + } + + @Test + public void authenticateWhenNotJwtExceptionThenPropagates() { + BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token-1"); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.error(new RuntimeException("Oops"))); + + assertThatCode(() -> this.manager.authenticate(token).block()) + .isInstanceOf(RuntimeException.class); + } + + @Test + public void authenticateWhenJwtThenSuccess() { + BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken("token-1"); + when(this.jwtDecoder.decode(token.getToken())).thenReturn(Mono.just(this.jwt)); + + Authentication authentication = this.manager.authenticate(token).block(); + + assertThat(authentication).isNotNull(); + assertThat(authentication.isAuthenticated()).isTrue(); + assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority).containsOnly("SCOPE_message:read", "SCOPE_message:write"); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPointTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPointTests.java new file mode 100644 index 00000000000..0515ccf225c --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPointTests.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link BearerTokenAuthenticationEntryPoint}. + * + * @author Vedran Pavic + * @author Josh Cummings + */ +public class BearerTokenAuthenticationEntryPointTests { + + private BearerTokenAuthenticationEntryPoint authenticationEntryPoint; + + @Before + public void setUp() { + this.authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint(); + } + + @Test + public void commenceWhenNoBearerTokenErrorThenStatus401AndAuthHeader() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + this.authenticationEntryPoint.commence(request, response, new BadCredentialsException("test")); + + assertThat(response.getStatus()).isEqualTo(401); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer"); + } + + @Test + public void commenceWhenNoBearerTokenErrorAndRealmSetThenStatus401AndAuthHeaderWithRealm() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + this.authenticationEntryPoint.setRealmName("test"); + this.authenticationEntryPoint.commence(request, response, new BadCredentialsException("test")); + + assertThat(response.getStatus()).isEqualTo(401); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer realm=\"test\""); + } + + @Test + public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithError() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError( + BearerTokenErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, + null, + null); + + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"invalid_request\""); + } + + @Test + public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithErrorDetails() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, HttpStatus.BAD_REQUEST, + "The access token expired", null, null); + + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getHeader("WWW-Authenticate")) + .isEqualTo("Bearer error=\"invalid_request\", error_description=\"The access token expired\""); + } + + @Test + public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithErrorUri() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, HttpStatus.BAD_REQUEST, + null, "http://example.com", null); + + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getHeader("WWW-Authenticate")) + .isEqualTo("Bearer error=\"invalid_request\", error_uri=\"http://example.com\""); + } + + @Test + public void commenceWhenInvalidTokenErrorThenStatus401AndHeaderWithError() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED, + null, null); + + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(401); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"invalid_token\""); + } + + @Test + public void commenceWhenInsufficientScopeErrorThenStatus403AndHeaderWithError() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN, + null, null); + + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\""); + } + + @Test + public void commenceWhenInsufficientScopeErrorThenStatus403AndHeaderWithErrorAndScope() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN, + null, null, "test.read test.write"); + + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")) + .isEqualTo("Bearer error=\"insufficient_scope\", scope=\"test.read test.write\""); + } + + @Test + public void commenceWhenInsufficientScopeAndRealmSetThenStatus403AndHeaderWithErrorAndAllDetails() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN, + "Insufficient scope", "http://example.com", "test.read test.write"); + + this.authenticationEntryPoint.setRealmName("test"); + this.authenticationEntryPoint.commence(request, response, + new OAuth2AuthenticationException(error)); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo( + "Bearer realm=\"test\", error=\"insufficient_scope\", error_description=\"Insufficient scope\", " + + "error_uri=\"http://example.com\", scope=\"test.read test.write\""); + } + + @Test + public void setRealmNameWhenNullRealmNameThenNoExceptionThrown() { + assertThatCode(() -> this.authenticationEntryPoint.setRealmName(null)) + .doesNotThrowAnyException(); + } + +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java new file mode 100644 index 00000000000..56bc183b4c3 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationFilterTests.java @@ -0,0 +1,173 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import java.io.IOException; +import javax.servlet.ServletException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes; +import org.springframework.security.web.AuthenticationEntryPoint; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * Tests {@link BearerTokenAuthenticationFilterTests} + * + * @author Josh Cummings + */ +@RunWith(MockitoJUnitRunner.class) +public class BearerTokenAuthenticationFilterTests { + @Mock + AuthenticationEntryPoint authenticationEntryPoint; + + @Mock + AuthenticationManager authenticationManager; + + @Mock + BearerTokenResolver bearerTokenResolver; + + MockHttpServletRequest request; + + MockHttpServletResponse response; + + MockFilterChain filterChain; + + @InjectMocks + BearerTokenAuthenticationFilter filter; + + @Before + public void httpMocks() { + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + this.filterChain = new MockFilterChain(); + } + + @Before + public void setterMocks() { + this.filter.setAuthenticationEntryPoint(this.authenticationEntryPoint); + this.filter.setBearerTokenResolver(this.bearerTokenResolver); + } + + @Test + public void doFilterWhenBearerTokenPresentThenAuthenticates() throws ServletException, IOException { + when(this.bearerTokenResolver.resolve(this.request)).thenReturn("token"); + + this.filter.doFilter(this.request, this.response, this.filterChain); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(BearerTokenAuthenticationToken.class); + + verify(this.authenticationManager).authenticate(captor.capture()); + + assertThat(captor.getValue().getPrincipal()).isEqualTo("token"); + } + + @Test + public void doFilterWhenNoBearerTokenPresentThenDoesNotAuthenticate() + throws ServletException, IOException { + + when(this.bearerTokenResolver.resolve(this.request)).thenReturn(null); + + dontAuthenticate(); + } + + @Test + public void doFilterWhenMalformedBearerTokenThenPropagatesError() throws ServletException, IOException { + BearerTokenError error = new BearerTokenError( + BearerTokenErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, + "description", + "uri"); + + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error); + + when(this.bearerTokenResolver.resolve(this.request)).thenThrow(exception); + + dontAuthenticate(); + + verify(this.authenticationEntryPoint).commence(this.request, this.response, exception); + } + + @Test + public void doFilterWhenAuthenticationFailsThenPropagatesError() throws ServletException, IOException { + BearerTokenError error = new BearerTokenError( + BearerTokenErrorCodes.INVALID_TOKEN, + HttpStatus.UNAUTHORIZED, + "description", + "uri" + ); + + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error); + + when(this.bearerTokenResolver.resolve(this.request)).thenReturn("token"); + when(this.authenticationManager.authenticate(any(BearerTokenAuthenticationToken.class))) + .thenThrow(exception); + + this.filter.doFilter(this.request, this.response, this.filterChain); + + verify(this.authenticationEntryPoint).commence(this.request, this.response, exception); + } + + @Test + public void setAuthenticationEntryPointWhenNullThenThrowsException() { + assertThatCode(() -> this.filter.setAuthenticationEntryPoint(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("authenticationEntryPoint cannot be null"); + } + + @Test + public void setBearerTokenResolverWhenNullThenThrowsException() { + assertThatCode(() -> this.filter.setBearerTokenResolver(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("bearerTokenResolver cannot be null"); + } + + @Test + public void constructorWhenNullAuthenticationManagerThenThrowsException() { + assertThatCode(() -> new BearerTokenAuthenticationFilter(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("authenticationManager cannot be null"); + } + + private void dontAuthenticate() + throws ServletException, IOException { + + this.filter.doFilter(this.request, this.response, this.filterChain); + + verifyNoMoreInteractions(this.authenticationManager); + } +} 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 new file mode 100644 index 00000000000..32518f4d1a6 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web; + +import java.util.Base64; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link DefaultBearerTokenResolver}. + * + * @author Vedran Pavic + */ +public class DefaultBearerTokenResolverTests { + + private static final String TEST_TOKEN = "test-token"; + + private DefaultBearerTokenResolver resolver; + + @Before + public void setUp() { + this.resolver = new DefaultBearerTokenResolver(); + } + + @Test + public void resolveWhenValidHeaderIsPresentThenTokenIsResolved() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + TEST_TOKEN); + + assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN); + } + + @Test + public void resolveWhenNoHeaderIsPresentThenTokenIsNotResolved() { + MockHttpServletRequest request = new MockHttpServletRequest(); + + assertThat(this.resolver.resolve(request)).isNull(); + } + + @Test + public void resolveWhenHeaderWithWrongSchemeIsPresentThenTokenIsNotResolved() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString("test:test".getBytes())); + + assertThat(this.resolver.resolve(request)).isNull(); + } + + @Test + public void resolveWhenHeaderWithMissingTokenIsPresentThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer "); + + assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining(("Bearer token is malformed")); + } + + @Test + public void resolveWhenHeaderWithInvalidCharactersIsPresentThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer an\"invalid\"token"); + + assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining(("Bearer token is malformed")); + } + + @Test + public void resolveWhenValidHeaderIsPresentTogetherWithFormParameterThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + TEST_TOKEN); + request.setMethod("POST"); + request.setContentType("application/x-www-form-urlencoded"); + request.addParameter("access_token", TEST_TOKEN); + + assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("Found multiple bearer tokens in the request"); + } + + @Test + public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + TEST_TOKEN); + request.setMethod("GET"); + request.addParameter("access_token", TEST_TOKEN); + + assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("Found multiple bearer tokens in the request"); + } + + @Test + public void resolveWhenRequestContainsTwoAccessTokenParametersThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("access_token", "token1", "token2"); + + assertThatCode(() -> this.resolver.resolve(request)).isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("Found multiple bearer tokens in the request"); + } + + @Test + public void resolveWhenFormParameterIsPresentAndSupportedThenTokenIsResolved() { + this.resolver.setAllowFormEncodedBodyParameter(true); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setContentType("application/x-www-form-urlencoded"); + request.addParameter("access_token", TEST_TOKEN); + + assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN); + } + + @Test + public void resolveWhenFormParameterIsPresentAndNotSupportedThenTokenIsNotResolved() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setContentType("application/x-www-form-urlencoded"); + request.addParameter("access_token", TEST_TOKEN); + + assertThat(this.resolver.resolve(request)).isNull(); + } + + @Test + public void resolveWhenQueryParameterIsPresentAndSupportedThenTokenIsResolved() { + this.resolver.setAllowUriQueryParameter(true); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("access_token", TEST_TOKEN); + + assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN); + } + + @Test + public void resolveWhenQueryParameterIsPresentAndNotSupportedThenTokenIsNotResolved() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.addParameter("access_token", TEST_TOKEN); + + assertThat(this.resolver.resolve(request)).isNull(); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java new file mode 100644 index 00000000000..fd9b598a398 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandlerTests.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.access; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import org.assertj.core.util.Maps; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link BearerTokenAccessDeniedHandlerTests} + * + * @author Josh Cummings + */ +public class BearerTokenAccessDeniedHandlerTests { + private BearerTokenAccessDeniedHandler accessDeniedHandler; + + @Before + public void setUp() { + this.accessDeniedHandler = new BearerTokenAccessDeniedHandler(); + } + + @Test + public void handleWhenNotOAuth2AuthenticatedThenStatus403() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Authentication authentication = new TestingAuthenticationToken("user", "pass"); + request.setUserPrincipal(authentication); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer"); + } + + @Test + public void handleWhenNotOAuth2AuthenticatedAndRealmSetThenStatus403AndAuthHeaderWithRealm() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Authentication authentication = new TestingAuthenticationToken("user", "pass"); + request.setUserPrincipal(authentication); + + this.accessDeniedHandler.setRealmName("test"); + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer realm=\"test\""); + } + + @Test + public void handleWhenTokenHasNoScopesThenInsufficientScopeError() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Authentication token = new TestingOAuth2TokenAuthenticationToken(Collections.emptyMap()); + request.setUserPrincipal(token); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } + + + @Test + public void handleWhenTokenHasScopeAttributeThenInsufficientScopeErrorWithScopes() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Map attributes = Maps.newHashMap("scope", "message:read message:write"); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + request.setUserPrincipal(token); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " + + "scope=\"message:read message:write\""); + } + + @Test + public void handleWhenTokenHasEmptyScopeAttributeThenInsufficientScopeError() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Map attributes = Maps.newHashMap("scope", ""); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + request.setUserPrincipal(token); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } + + @Test + public void handleWhenTokenHasScpAttributeThenInsufficientScopeErrorWithScopes() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Map attributes = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write")); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + request.setUserPrincipal(token); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " + + "scope=\"message:read message:write\""); + } + + @Test + public void handleWhenTokenHasEmptyScpAttributeThenInsufficientScopeError() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Map attributes = Maps.newHashMap("scp", Collections.emptyList()); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + request.setUserPrincipal(token); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } + + @Test + public void handleWhenTokenHasBothScopeAndScpAttributesTheInsufficientErrorBasedOnScopeAttribute() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Map attributes = Maps.newHashMap("scp", Arrays.asList("message:read", "message:write")); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + request.setUserPrincipal(token); + attributes.put("scope", "missive:read missive:write"); + + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [missive:read missive:write] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " + + "scope=\"missive:read missive:write\""); + } + + @Test + public void handleWhenTokenHasScopeAttributeAndRealmIsSetThenInsufficientScopeErrorWithScopesAndRealm() + throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Map attributes = Maps.newHashMap("scope", "message:read message:write"); + Authentication token = new TestingOAuth2TokenAuthenticationToken(attributes); + request.setUserPrincipal(token); + + this.accessDeniedHandler.setRealmName("test"); + this.accessDeniedHandler.handle(request, response, null); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getHeader("WWW-Authenticate")).isEqualTo("Bearer realm=\"test\", " + + "error=\"insufficient_scope\", " + + "error_description=\"The token provided has insufficient scope [message:read message:write] for this request\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\", " + + "scope=\"message:read message:write\""); + } + + @Test + public void setRealmNameWhenNullRealmNameThenNoExceptionThrown() { + assertThatCode(() -> this.accessDeniedHandler.setRealmName(null)) + .doesNotThrowAnyException(); + } + + static class TestingOAuth2TokenAuthenticationToken + extends AbstractOAuth2TokenAuthenticationToken { + + private Map attributes; + + protected TestingOAuth2TokenAuthenticationToken(Map attributes) { + super(new TestingOAuth2Token("token")); + this.attributes = attributes; + } + + @Override + public Map getTokenAttributes() { + return this.attributes; + } + + static class TestingOAuth2Token extends AbstractOAuth2Token { + public TestingOAuth2Token(String tokenValue) { + super(tokenValue); + } + } + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPointTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPointTests.java new file mode 100644 index 00000000000..cb60fd01312 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPointTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.server; + +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.MockServerHttpResponse; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.server.resource.BearerTokenError; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class BearerTokenServerAuthenticationEntryPointTests { + private BearerTokenServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint(); + + private MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + + @Test + public void commenceWhenNotOAuth2AuthenticationExceptionThenBearer() { + this.entryPoint.commence(this.exchange, new BadCredentialsException("")).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer"); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void commenceWhenRealmNameThenHasRealmName() { + this.entryPoint.setRealmName("Realm"); + + this.entryPoint.commence(this.exchange, new BadCredentialsException("")).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer realm=\"Realm\""); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void commenceWhenOAuth2AuthenticationExceptionThenContainsErrorInformation() { + OAuth2Error oauthError = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(oauthError); + + this.entryPoint.commence(this.exchange, exception).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer error=\"invalid_request\""); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void commenceWhenOAuth2ErrorCompleteThenContainsErrorInformation() { + OAuth2Error oauthError = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "Oops", "https://example.com"); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(oauthError); + + this.entryPoint.commence(this.exchange, exception).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer error=\"invalid_request\", error_description=\"Oops\", error_uri=\"https://example.com\""); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void commenceWhenBearerTokenThenErrorInformation() { + OAuth2Error oauthError = new BearerTokenError(OAuth2ErrorCodes.INVALID_REQUEST, + HttpStatus.BAD_REQUEST, "Oops", "https://example.com"); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(oauthError); + + this.entryPoint.commence(this.exchange, exception).block(); + + assertThat(getResponse().getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)).isEqualTo("Bearer error=\"invalid_request\", error_description=\"Oops\", error_uri=\"https://example.com\""); + assertThat(getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + private MockServerHttpResponse getResponse() { + return this.exchange.getResponse(); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java new file mode 100644 index 00000000000..3326d17c9a1 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.web.server; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; + +import java.util.Base64; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class ServerBearerTokenAuthenticationConverterTests { + private static final String TEST_TOKEN = "test-token"; + + private ServerBearerTokenAuthenticationConverter converter; + + @Before + public void setup() { + this.converter = new ServerBearerTokenAuthenticationConverter(); + } + + @Test + public void resolveWhenValidHeaderIsPresentThenTokenIsResolved() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_TOKEN); + + assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN); + } + + @Test + public void resolveWhenNoHeaderIsPresentThenTokenIsNotResolved() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/"); + + assertThat(convertToToken(request)).isNull(); + } + + @Test + public void resolveWhenHeaderWithWrongSchemeIsPresentThenTokenIsNotResolved() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString("test:test".getBytes())); + + assertThat(convertToToken(request)).isNull(); + } + + @Test + public void resolveWhenHeaderWithMissingTokenIsPresentThenAuthenticationExceptionIsThrown() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer "); + + assertThatCode(() -> convertToToken(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining(("Bearer token is malformed")); + } + + @Test + public void resolveWhenHeaderWithInvalidCharactersIsPresentThenAuthenticationExceptionIsThrown() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer an\"invalid\"token"); + + assertThatCode(() -> convertToToken(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining(("Bearer token is malformed")); + } + + @Test + public void resolveWhenValidHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .queryParam("access_token", TEST_TOKEN) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_TOKEN); + + assertThatCode(() -> convertToToken(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("Found multiple bearer tokens in the request"); + } + + @Test + public void resolveWhenQueryParameterIsPresentAndSupportedThenTokenIsResolved() { + this.converter.setAllowUriQueryParameter(true); + + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .queryParam("access_token", TEST_TOKEN); + + assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN); + } + + @Test + public void resolveWhenQueryParameterIsPresentAndNotSupportedThenTokenIsNotResolved() { + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .queryParam("access_token", TEST_TOKEN); + + assertThat(convertToToken(request)).isNull(); + } + + private BearerTokenAuthenticationToken convertToToken(MockServerHttpRequest.BaseBuilder request) { + return convertToToken(request.build()); + } + + private BearerTokenAuthenticationToken convertToToken(MockServerHttpRequest request) { + MockServerWebExchange exchange = MockServerWebExchange.from(request); + return this.converter.apply(exchange).cast(BearerTokenAuthenticationToken.class).block(); + } +} diff --git a/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java b/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java index fefb628c3b1..ba613a8cc7a 100644 --- a/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java +++ b/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java @@ -216,7 +216,7 @@ public void testIgnoresUserPassAuthToken() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( USERNAME, "password"); - assertThat(provider.authenticate(token)).isEqualTo(null); + assertThat(provider.authenticate(token)).isNull(); } /* diff --git a/samples/boot/oauth2/authcodegrant/README.adoc b/samples/boot/authcodegrant/README.adoc similarity index 100% rename from samples/boot/oauth2/authcodegrant/README.adoc rename to samples/boot/authcodegrant/README.adoc diff --git a/samples/boot/oauth2/authcodegrant/spring-security-samples-boot-oauth2-authcodegrant.gradle b/samples/boot/authcodegrant/spring-security-samples-boot-authcodegrant.gradle similarity index 91% rename from samples/boot/oauth2/authcodegrant/spring-security-samples-boot-oauth2-authcodegrant.gradle rename to samples/boot/authcodegrant/spring-security-samples-boot-authcodegrant.gradle index 572fe40331c..dc46d8f456b 100644 --- a/samples/boot/oauth2/authcodegrant/spring-security-samples-boot-oauth2-authcodegrant.gradle +++ b/samples/boot/authcodegrant/spring-security-samples-boot-authcodegrant.gradle @@ -9,7 +9,7 @@ dependencies { compile 'org.springframework.boot:spring-boot-starter-thymeleaf' compile 'org.springframework.boot:spring-boot-starter-web' compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4' - compile 'io.projectreactor.ipc:reactor-netty' + compile 'io.projectreactor.netty:reactor-netty' testCompile project(':spring-security-test') testCompile 'org.springframework.boot:spring-boot-starter-test' diff --git a/samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java b/samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java similarity index 83% rename from samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java rename to samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java index 59df61ff536..d94e4b0470c 100644 --- a/samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java +++ b/samples/boot/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java @@ -22,23 +22,29 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; @@ -47,6 +53,7 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import sample.config.WebClientConfig; import java.util.HashMap; import java.util.Map; @@ -55,6 +62,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; @@ -76,7 +84,7 @@ public class OAuth2AuthorizationCodeGrantApplicationTests { private ClientRegistrationRepository clientRegistrationRepository; @Autowired - private OAuth2AuthorizedClientService authorizedClientService; + private OAuth2AuthorizedClientRepository authorizedClientRepository; @Autowired private MockMvc mockMvc; @@ -86,7 +94,7 @@ public void requestWhenClientNotAuthorizedThenRedirectForAuthorization() throws MvcResult mvcResult = this.mockMvc.perform(get("/repos").with(user("user"))) .andExpect(status().is3xxRedirection()) .andReturn(); - assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://github.com/login/oauth/authorize\\?response_type=code&client_id=your-app-client-id&scope=public_repo&state=.{15,}&redirect_uri=http://localhost/github-repos"); + assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://github.com/login/oauth/authorize\\?response_type=code&client_id=your-app-client-id&scope=public_repo&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fgithub-repos"); } @Test @@ -114,18 +122,19 @@ public void requestWhenClientGrantedAuthorizationThenAuthorizedClientSaved() thr MockHttpSession session = (MockHttpSession) request.getSession(); String principalName = "user"; + TestingAuthenticationToken authentication = new TestingAuthenticationToken(principalName, "password"); // Authorization Response this.mockMvc.perform(get("/github-repos") .param(OAuth2ParameterNames.CODE, "code") .param(OAuth2ParameterNames.STATE, "state") - .with(user(principalName)) + .with(authentication(authentication)) .session(session)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("http://localhost/github-repos")); - OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient( - registration.getRegistrationId(), principalName); + OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient( + registration.getRegistrationId(), authentication, request); assertThat(authorizedClient).isNotNull(); } @@ -160,6 +169,17 @@ private OAuth2AccessTokenResponseClient acc @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(basePackages = "sample.web") + @Import(WebClientConfig.class) public static class SpringBootApplicationTestConfig { + + @Bean + public OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) { + return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); + } + + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); + } } } diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java b/samples/boot/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java similarity index 100% rename from samples/boot/oauth2/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java rename to samples/boot/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/SecurityConfig.java b/samples/boot/authcodegrant/src/main/java/sample/config/SecurityConfig.java similarity index 78% rename from samples/boot/oauth2/authcodegrant/src/main/java/sample/config/SecurityConfig.java rename to samples/boot/authcodegrant/src/main/java/sample/config/SecurityConfig.java index e755450b2d8..3a47ac24c26 100644 --- a/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/SecurityConfig.java +++ b/samples/boot/authcodegrant/src/main/java/sample/config/SecurityConfig.java @@ -22,6 +22,9 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.provisioning.InMemoryUserDetailsManager; /** @@ -43,6 +46,11 @@ protected void configure(HttpSecurity http) throws Exception { .authorizationCodeGrant(); } + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); + } + @Bean public UserDetailsService userDetailsService() { UserDetails userDetails = User.withDefaultPasswordEncoder() diff --git a/samples/boot/authcodegrant/src/main/java/sample/config/WebClientConfig.java b/samples/boot/authcodegrant/src/main/java/sample/config/WebClientConfig.java new file mode 100644 index 00000000000..fe714c7b5b5 --- /dev/null +++ b/samples/boot/authcodegrant/src/main/java/sample/config/WebClientConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 sample.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author Rob Winch + * @since 5.1 + */ +@Configuration +public class WebClientConfig { + + @Bean + WebClient webClient() { + return WebClient.builder() + .filter(new ServletOAuth2AuthorizedClientExchangeFilterFunction()) + .build(); + } +} diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/web/GitHubReposController.java b/samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java similarity index 59% rename from samples/boot/oauth2/authcodegrant/src/main/java/sample/web/GitHubReposController.java rename to samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java index f32bdfe450a..2e85edc0f9b 100644 --- a/samples/boot/oauth2/authcodegrant/src/main/java/sample/web/GitHubReposController.java +++ b/samples/boot/authcodegrant/src/main/java/sample/web/GitHubReposController.java @@ -15,24 +15,28 @@ */ package sample.web; -import org.springframework.http.HttpHeaders; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.reactive.function.client.ClientRequest; -import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; import java.util.List; +import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; + /** * @author Joe Grandja + * @author Rob Winch */ @Controller public class GitHubReposController { + private final WebClient webClient; + + public GitHubReposController(WebClient webClient) { + this.webClient = webClient; + } @GetMapping("/") public String index() { @@ -40,13 +44,12 @@ public String index() { } @GetMapping("/repos") - public String gitHubRepos(Model model, @OAuth2Client("github") OAuth2AuthorizedClient authorizedClient) { + public String gitHubRepos(Model model, @RegisteredOAuth2AuthorizedClient("github") OAuth2AuthorizedClient authorizedClient) { String endpointUri = "https://api.github.com/user/repos"; - List repos = WebClient.builder() - .filter(oauth2Credentials(authorizedClient)) - .build() + List repos = this.webClient .get() .uri(endpointUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) .retrieve() .bodyToMono(List.class) .block(); @@ -54,14 +57,4 @@ public String gitHubRepos(Model model, @OAuth2Client("github") OAuth2AuthorizedC return "github-repos"; } - - private ExchangeFilterFunction oauth2Credentials(OAuth2AuthorizedClient authorizedClient) { - return ExchangeFilterFunction.ofRequestProcessor( - clientRequest -> { - ClientRequest authorizedRequest = ClientRequest.from(clientRequest) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue()) - .build(); - return Mono.just(authorizedRequest); - }); - } } diff --git a/samples/boot/oauth2/authcodegrant/src/main/resources/application.yml b/samples/boot/authcodegrant/src/main/resources/application.yml similarity index 100% rename from samples/boot/oauth2/authcodegrant/src/main/resources/application.yml rename to samples/boot/authcodegrant/src/main/resources/application.yml diff --git a/samples/boot/oauth2/authcodegrant/src/main/resources/templates/github-repos.html b/samples/boot/authcodegrant/src/main/resources/templates/github-repos.html similarity index 100% rename from samples/boot/oauth2/authcodegrant/src/main/resources/templates/github-repos.html rename to samples/boot/authcodegrant/src/main/resources/templates/github-repos.html diff --git a/samples/boot/hellowebflux-method/spring-security-samples-boot-hellowebflux-method.gradle b/samples/boot/hellowebflux-method/spring-security-samples-boot-hellowebflux-method.gradle new file mode 100644 index 00000000000..c21781806f8 --- /dev/null +++ b/samples/boot/hellowebflux-method/spring-security-samples-boot-hellowebflux-method.gradle @@ -0,0 +1,12 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-security-core') + compile project(':spring-security-config') + compile project(':spring-security-web') + compile 'org.springframework.boot:spring-boot-starter-webflux' + + testCompile project(':spring-security-test') + testCompile 'io.projectreactor:reactor-test' + testCompile 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/samples/boot/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java b/samples/boot/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java new file mode 100644 index 00000000000..f474e2c0a31 --- /dev/null +++ b/samples/boot/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java @@ -0,0 +1,79 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://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 sample; + +import java.util.function.Consumer; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author Rob Winch + * @since 5.0 + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class HelloWebfluxMethodApplicationITests { + + @Autowired + WebTestClient rest; + + + @Test + public void messageWhenNotAuthenticated() throws Exception { + this.rest + .get() + .uri("/message") + .exchange() + .expectStatus().isUnauthorized(); + } + + @Test + public void messageWhenUserThenForbidden() throws Exception { + this.rest + .get() + .uri("/message") + .headers(robsCredentials()) + .exchange() + .expectStatus().isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + public void messageWhenAdminThenOk() throws Exception { + this.rest + .get() + .uri("/message") + .headers(adminCredentials()) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello World!"); + } + + private Consumer robsCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("rob", "rob"); + } + + private Consumer adminCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("admin", "admin"); + } +} + diff --git a/samples/boot/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java b/samples/boot/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java new file mode 100644 index 00000000000..85aa92bb61b --- /dev/null +++ b/samples/boot/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java @@ -0,0 +1,32 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://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 sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Rob Winch + * @since 5.0 + */ +@SpringBootApplication +public class HelloWebfluxMethodApplication { + + public static void main(String[] args) { + SpringApplication.run(HelloWebfluxMethodApplication.class, args); + } +} diff --git a/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWorldMessageService.java b/samples/boot/hellowebflux-method/src/main/java/sample/HelloWorldMessageService.java similarity index 100% rename from samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWorldMessageService.java rename to samples/boot/hellowebflux-method/src/main/java/sample/HelloWorldMessageService.java diff --git a/samples/javaconfig/hellowebflux-method/src/main/java/sample/MessageController.java b/samples/boot/hellowebflux-method/src/main/java/sample/MessageController.java similarity index 100% rename from samples/javaconfig/hellowebflux-method/src/main/java/sample/MessageController.java rename to samples/boot/hellowebflux-method/src/main/java/sample/MessageController.java diff --git a/samples/javaconfig/hellowebflux-method/src/main/java/sample/SecurityConfig.java b/samples/boot/hellowebflux-method/src/main/java/sample/SecurityConfig.java similarity index 100% rename from samples/javaconfig/hellowebflux-method/src/main/java/sample/SecurityConfig.java rename to samples/boot/hellowebflux-method/src/main/java/sample/SecurityConfig.java diff --git a/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java b/samples/boot/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java similarity index 75% rename from samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java rename to samples/boot/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java index 88e2c629c3c..635d9aa6d7d 100644 --- a/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java +++ b/samples/boot/hellowebflux-method/src/test/java/sample/HelloWebfluxMethodApplicationTests.java @@ -15,48 +15,38 @@ */ package sample; -import java.util.Map; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; + import java.util.function.Consumer; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; - /** * @author Rob Winch * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxMethodApplication.class) -@ActiveProfiles("test") +@SpringBootTest public class HelloWebfluxMethodApplicationTests { - @Autowired - ApplicationContext context; - WebTestClient rest; - @Before - public void setup() { + @Autowired + public void setup(ApplicationContext context) { this.rest = WebTestClient - .bindToApplicationContext(this.context) - .apply(springSecurity()) - .configureClient() - .filter(basicAuthentication()) - .build(); + .bindToApplicationContext(context) + .apply(springSecurity()) + .configureClient() + .build(); } @Test @@ -75,7 +65,7 @@ public void messageWhenUserThenForbidden() throws Exception { this.rest .get() .uri("/message") - .attributes(robsCredentials()) + .headers(robsCredentials()) .exchange() .expectStatus().isEqualTo(HttpStatus.FORBIDDEN); } @@ -85,7 +75,7 @@ public void messageWhenAdminThenOk() throws Exception { this.rest .get() .uri("/message") - .attributes(adminCredentials()) + .headers(adminCredentials()) .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hello World!"); @@ -137,11 +127,11 @@ public void messageWhenMutateWithMockAdminThenOk() throws Exception { .expectBody(String.class).isEqualTo("Hello World!"); } - private Consumer> robsCredentials() { - return basicAuthenticationCredentials("rob", "rob"); + private Consumer robsCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("rob", "rob"); } - private Consumer> adminCredentials() { - return basicAuthenticationCredentials("admin", "admin"); + private Consumer adminCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("admin", "admin"); } } diff --git a/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java b/samples/boot/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java similarity index 88% rename from samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java rename to samples/boot/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java index c33a0accafa..9989b9d030c 100644 --- a/samples/javaconfig/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java +++ b/samples/boot/hellowebflux-method/src/test/java/sample/HelloWorldMessageServiceTests.java @@ -19,10 +19,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import reactor.test.StepVerifier; @@ -31,8 +30,7 @@ * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxMethodApplication.class) -@ActiveProfiles("test") +@SpringBootTest public class HelloWorldMessageServiceTests { @Autowired HelloWorldMessageService messages; diff --git a/samples/boot/hellowebflux/spring-security-samples-boot-hellowebflux.gradle b/samples/boot/hellowebflux/spring-security-samples-boot-hellowebflux.gradle new file mode 100644 index 00000000000..d65d168ad11 --- /dev/null +++ b/samples/boot/hellowebflux/spring-security-samples-boot-hellowebflux.gradle @@ -0,0 +1,11 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-security-core') + compile project(':spring-security-config') + compile project(':spring-security-web') + compile 'org.springframework.boot:spring-boot-starter-webflux' + + testCompile project(':spring-security-test') + testCompile 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/samples/javaconfig/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java b/samples/boot/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java similarity index 57% rename from samples/javaconfig/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java rename to samples/boot/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java index bc1abf00ecc..2629ab5a95d 100644 --- a/samples/javaconfig/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java +++ b/samples/boot/hellowebflux/src/integration-test/java/sample/HelloWebfluxApplicationITests.java @@ -15,46 +15,27 @@ */ package sample; -import java.time.Duration; -import java.util.Map; import java.util.function.Consumer; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; - /** * @author Rob Winch * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxApplication.class) -@TestPropertySource(properties = "server.port=0") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class HelloWebfluxApplicationITests { - @Value("#{@nettyContext.address().getPort()}") - int port; + @Autowired WebTestClient rest; - @Before - public void setup() { - this.rest = WebTestClient.bindToServer() - .responseTimeout(Duration.ofDays(1)) - .baseUrl("http://localhost:" + this.port) - .filter(basicAuthentication()) - .build(); - } - - @Test public void basicWhenNoCredentialsThenUnauthorized() throws Exception { this.rest @@ -69,7 +50,7 @@ public void basicWhenValidCredentialsThenOk() throws Exception { this.rest .get() .uri("/") - .attributes(userCredentials()) + .headers(userCredentials()) .exchange() .expectStatus().isOk() .expectBody().json("{\"message\":\"Hello user!\"}"); @@ -80,17 +61,17 @@ public void basicWhenInvalidCredentialsThenUnauthorized() throws Exception { this.rest .get() .uri("/") - .attributes(invalidCredentials()) + .headers(invalidCredentials()) .exchange() .expectStatus().isUnauthorized() .expectBody().isEmpty(); } - private Consumer> userCredentials() { - return basicAuthenticationCredentials("user", "user"); + private Consumer userCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "user"); } - private Consumer> invalidCredentials() { - return basicAuthenticationCredentials("user", "INVALID"); + private Consumer invalidCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "INVALID"); } } diff --git a/samples/javaconfig/hellowebflux/src/main/java/sample/HelloUserController.java b/samples/boot/hellowebflux/src/main/java/sample/HelloUserController.java similarity index 100% rename from samples/javaconfig/hellowebflux/src/main/java/sample/HelloUserController.java rename to samples/boot/hellowebflux/src/main/java/sample/HelloUserController.java diff --git a/samples/boot/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java b/samples/boot/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java new file mode 100644 index 00000000000..f47de63567f --- /dev/null +++ b/samples/boot/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java @@ -0,0 +1,33 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://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 sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Rob Winch + * @since 5.0 + */ +@SpringBootApplication +public class HelloWebfluxApplication { + + public static void main(String[] args) { + SpringApplication.run(HelloWebfluxApplication.class, args); + } + +} diff --git a/samples/javaconfig/hellowebflux/src/main/java/sample/HelloWebfluxSecurityConfig.java b/samples/boot/hellowebflux/src/main/java/sample/HelloWebfluxSecurityConfig.java similarity index 100% rename from samples/javaconfig/hellowebflux/src/main/java/sample/HelloWebfluxSecurityConfig.java rename to samples/boot/hellowebflux/src/main/java/sample/HelloWebfluxSecurityConfig.java diff --git a/samples/javaconfig/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java b/samples/boot/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java similarity index 71% rename from samples/javaconfig/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java rename to samples/boot/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java index e3a840b05e7..eb3c8af20d4 100644 --- a/samples/javaconfig/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java +++ b/samples/boot/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java @@ -15,47 +15,39 @@ */ package sample; -import java.util.Map; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; + import java.util.function.Consumer; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpHeaders; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; - /** * @author Rob Winch * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxApplication.class) -@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureWebTestClient public class HelloWebfluxApplicationTests { - @Autowired - ApplicationContext context; - WebTestClient rest; - @Before - public void setup() { + @Autowired + public void setup(ApplicationContext context) { this.rest = WebTestClient - .bindToApplicationContext(this.context) - .apply(springSecurity()) - .configureClient() - .filter(basicAuthentication()) - .build(); + .bindToApplicationContext(context) + .apply(springSecurity()) + .configureClient() + .build(); } @Test @@ -72,7 +64,7 @@ public void basicWhenValidCredentialsThenOk() throws Exception { this.rest .get() .uri("/") - .attributes(userCredentials()) + .headers(userCredentials()) .exchange() .expectStatus().isOk() .expectBody().json("{\"message\":\"Hello user!\"}"); @@ -83,7 +75,7 @@ public void basicWhenInvalidCredentialsThenUnauthorized() throws Exception { this.rest .get() .uri("/") - .attributes(invalidCredentials()) + .headers(invalidCredentials()) .exchange() .expectStatus().isUnauthorized() .expectBody().isEmpty(); @@ -111,11 +103,11 @@ public void mockSupportWhenWithMockUserThenOk() throws Exception { .expectBody().json("{\"message\":\"Hello user!\"}"); } - private Consumer> userCredentials() { - return basicAuthenticationCredentials("user", "user"); + private Consumer userCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "user"); } - private Consumer> invalidCredentials() { - return basicAuthenticationCredentials("user", "INVALID"); + private Consumer invalidCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "INVALID"); } } diff --git a/samples/boot/hellowebfluxfn/spring-security-samples-boot-hellowebfluxfn.gradle b/samples/boot/hellowebfluxfn/spring-security-samples-boot-hellowebfluxfn.gradle new file mode 100644 index 00000000000..d65d168ad11 --- /dev/null +++ b/samples/boot/hellowebfluxfn/spring-security-samples-boot-hellowebfluxfn.gradle @@ -0,0 +1,11 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-security-core') + compile project(':spring-security-config') + compile project(':spring-security-web') + compile 'org.springframework.boot:spring-boot-starter-webflux' + + testCompile project(':spring-security-test') + testCompile 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java b/samples/boot/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java similarity index 57% rename from samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java rename to samples/boot/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java index 280f8d7cc7a..2ca6eca77c0 100644 --- a/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java +++ b/samples/boot/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java @@ -15,43 +15,31 @@ */ package sample; -import java.time.Duration; -import java.util.Map; import java.util.function.Consumer; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; - -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; /** * @author Rob Winch * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxFnApplication.class) -@TestPropertySource(properties = "server.port=0") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class HelloWebfluxFnApplicationITests { - @Value("#{@nettyContext.address().getPort()}") - int port; WebTestClient rest; - @Before - public void setup() { - this.rest = WebTestClient.bindToServer() - .responseTimeout(Duration.ofDays(1)) - .baseUrl("http://localhost:" + this.port) - .filter(basicAuthentication()) - .build(); + @Autowired + public void setRest(WebTestClient rest) { + this.rest = rest + .mutateWith((b, h, c) -> b.filter(ExchangeFilterFunctions.basicAuthentication())); } @Test @@ -68,7 +56,7 @@ public void basicWhenValidCredentialsThenOk() throws Exception { this.rest .get() .uri("/") - .attributes(userCredentials()) + .headers(userCredentials()) .exchange() .expectStatus().isOk() .expectBody().json("{\"message\":\"Hello user!\"}"); @@ -79,17 +67,17 @@ public void basicWhenInvalidCredentialsThenUnauthorized() throws Exception { this.rest .get() .uri("/") - .attributes(invalidCredentials()) + .headers(invalidCredentials()) .exchange() .expectStatus().isUnauthorized() .expectBody().isEmpty(); } - private Consumer> userCredentials() { - return basicAuthenticationCredentials("user", "user"); + private Consumer userCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "user"); } - private Consumer> invalidCredentials() { - return basicAuthenticationCredentials("user", "INVALID"); + private Consumer invalidCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "INVALID"); } } diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloUserController.java b/samples/boot/hellowebfluxfn/src/main/java/sample/HelloUserController.java similarity index 100% rename from samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloUserController.java rename to samples/boot/hellowebfluxfn/src/main/java/sample/HelloUserController.java diff --git a/samples/boot/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java b/samples/boot/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java new file mode 100644 index 00000000000..bec73a10a5a --- /dev/null +++ b/samples/boot/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java @@ -0,0 +1,44 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://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 sample; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +/** + * @author Rob Winch + * @since 5.0 + */ +@SpringBootApplication +public class HelloWebfluxFnApplication { + + public static void main(String[] args) { + SpringApplication.run(HelloWebfluxFnApplication.class, args); + } + + @Bean + public RouterFunction routes(HelloUserController userController) { + return route( + GET("/"), userController::hello); + } +} diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnSecurityConfig.java b/samples/boot/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnSecurityConfig.java similarity index 100% rename from samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnSecurityConfig.java rename to samples/boot/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnSecurityConfig.java diff --git a/samples/javaconfig/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java b/samples/boot/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java similarity index 66% rename from samples/javaconfig/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java rename to samples/boot/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java index ac549cf79da..b2783cdc76b 100644 --- a/samples/javaconfig/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java +++ b/samples/boot/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java @@ -15,50 +15,40 @@ */ package sample; -import java.util.Map; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; + import java.util.function.Consumer; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpHeaders; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.security.web.server.WebFilterChainProxy; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.server.RouterFunction; - -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; /** * @author Rob Winch * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxFnApplication.class) -@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureWebTestClient public class HelloWebfluxFnApplicationTests { - @Autowired - RouterFunction routerFunction; - @Autowired WebFilterChainProxy springSecurityFilterChain; WebTestClient rest; - @Before - public void setup() { + @Autowired + public void setup(ApplicationContext context) { this.rest = WebTestClient - .bindToRouterFunction(this.routerFunction) - .webFilter(this.springSecurityFilterChain) - .apply(springSecurity()) - .configureClient() - .filter(basicAuthentication()) - .build(); + .bindToApplicationContext(context) + .apply(springSecurity()) + .configureClient() + .build(); } @Test @@ -75,7 +65,7 @@ public void basicWhenValidCredentialsThenOk() throws Exception { this.rest .get() .uri("/") - .attributes(userCredentials()) + .headers(userCredentials()) .exchange() .expectStatus().isOk() .expectBody().json("{\"message\":\"Hello user!\"}"); @@ -86,7 +76,7 @@ public void basicWhenInvalidCredentialsThenUnauthorized() throws Exception { this.rest .get() .uri("/") - .attributes(invalidCredentials()) + .headers(invalidCredentials()) .exchange() .expectStatus().isUnauthorized() .expectBody().isEmpty(); @@ -114,11 +104,11 @@ public void mockSupportWhenWithMockUserThenOk() throws Exception { .expectBody().json("{\"message\":\"Hello user!\"}"); } - private Consumer> userCredentials() { - return basicAuthenticationCredentials("user", "user"); + private Consumer userCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "user"); } - private Consumer> invalidCredentials() { - return basicAuthenticationCredentials("user", "INVALID"); + private Consumer invalidCredentials() { + return httpHeaders -> httpHeaders.setBasicAuth("user", "INVALID"); } } diff --git a/samples/boot/oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle b/samples/boot/oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle index 933d9079e9e..88e6d285d8d 100644 --- a/samples/boot/oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle +++ b/samples/boot/oauth2login-webflux/spring-security-samples-boot-oauth2login-webflux.gradle @@ -9,7 +9,6 @@ dependencies { compile 'org.springframework.boot:spring-boot-starter-thymeleaf' compile 'org.springframework.boot:spring-boot-starter-webflux' compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4' - compile 'io.projectreactor.ipc:reactor-netty' testCompile project(':spring-security-test') testCompile 'net.sourceforge.htmlunit:htmlunit' diff --git a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java b/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java deleted file mode 100644 index dd3d788fe94..00000000000 --- a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientAutoConfiguration.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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.boot.autoconfigure.security.oauth2.client; - -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; -import org.springframework.security.oauth2.client.registration.ClientRegistration; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for OAuth client support. - * - * @author Madhura Bhave - * @author Phillip Webb - * @since 2.0.0 - */ -@Configuration -@AutoConfigureBefore(name = "org.springframework.boot.autoconfigure.security.reactive.WebFluxSecurityConfiguration") -@ConditionalOnClass({ EnableWebFluxSecurity.class, ClientRegistration.class }) -@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) -@Import({ ReactiveOAuth2ClientRegistrationRepositoryConfiguration.class, - ReactiveOAuth2WebSecurityConfiguration.class }) -public class ReactiveOAuth2ClientAutoConfiguration { - -} diff --git a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java b/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java deleted file mode 100644 index abd5e767a18..00000000000 --- a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2ClientRegistrationRepositoryConfiguration.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2012-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. - * You may obtain a copy of the License at - * - * http://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.boot.autoconfigure.security.oauth2.client; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; - -/** - * {@link Configuration} used to map {@link OAuth2ClientProperties} to client - * registrations. - * - * @author Madhura Bhave - * @author Phillip Webb - */ -@Configuration -@EnableConfigurationProperties(OAuth2ClientProperties.class) -@Conditional(OAuth2ClientRegistrationRepositoryConfiguration.ClientsConfiguredCondition.class) -class ReactiveOAuth2ClientRegistrationRepositoryConfiguration { - - private final OAuth2ClientProperties properties; - - ReactiveOAuth2ClientRegistrationRepositoryConfiguration(OAuth2ClientProperties properties) { - this.properties = properties; - } - - @Bean - @ConditionalOnMissingBean(ReactiveClientRegistrationRepository.class) - public InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() { - List registrations = new ArrayList<>( - OAuth2ClientPropertiesRegistrationAdapter - .getClientRegistrations(this.properties).values()); - return new InMemoryReactiveClientRegistrationRepository(registrations); - } -} diff --git a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java b/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java deleted file mode 100644 index 13234672d69..00000000000 --- a/samples/boot/oauth2login-webflux/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ReactiveOAuth2WebSecurityConfiguration.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2012-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. - * You may obtain a copy of the License at - * - * http://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.boot.autoconfigure.security.oauth2.client; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; - -/** - * {@link WebSecurityConfigurerAdapter} to add OAuth client support. - * - * @author Madhura Bhave - * @author Phillip Webb - * @since 2.0.0 - */ -@Configuration -@ConditionalOnBean(ReactiveClientRegistrationRepository.class) -class ReactiveOAuth2WebSecurityConfiguration { - - @Bean - @ConditionalOnMissingBean - public ReactiveOAuth2AuthorizedClientService authorizedClientService( - ReactiveClientRegistrationRepository clientRegistrationRepository) { - return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository); - } - -} diff --git a/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java b/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java index d3bcaf342e5..a73325ebdd4 100644 --- a/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java +++ b/samples/boot/oauth2login-webflux/src/main/java/sample/web/OAuth2LoginController.java @@ -16,20 +16,13 @@ package sample.web; -import java.util.Map; - -import org.springframework.http.HttpHeaders; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.reactive.function.client.ClientRequest; -import org.springframework.web.reactive.function.client.ExchangeFilterFunction; -import org.springframework.web.reactive.function.client.WebClient; - -import reactor.core.publisher.Mono; /** * @author Rob Winch @@ -38,37 +31,12 @@ public class OAuth2LoginController { @GetMapping("/") - public String index(Model model, @OAuth2Client OAuth2AuthorizedClient authorizedClient) { - model.addAttribute("userName", authorizedClient.getPrincipalName()); + public String index(Model model, + @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient, + @AuthenticationPrincipal OAuth2User oauth2User) { + model.addAttribute("userName", oauth2User.getName()); model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName()); + model.addAttribute("userAttributes", oauth2User.getAttributes()); return "index"; } - - @GetMapping("/userinfo") - public String userinfo(Model model, @OAuth2Client OAuth2AuthorizedClient authorizedClient) { - Mono userAttributes = Mono.empty(); - String userInfoEndpointUri = authorizedClient.getClientRegistration() - .getProviderDetails().getUserInfoEndpoint().getUri(); - if (!StringUtils.isEmpty(userInfoEndpointUri)) { // userInfoEndpointUri is optional for OIDC Clients - userAttributes = WebClient.builder() - .filter(oauth2Credentials(authorizedClient)) - .build() - .get() - .uri(userInfoEndpointUri) - .retrieve() - .bodyToMono(Map.class); - } - model.addAttribute("userAttributes", userAttributes); - return "userinfo"; - } - - private ExchangeFilterFunction oauth2Credentials(OAuth2AuthorizedClient authorizedClient) { - return ExchangeFilterFunction.ofRequestProcessor( - clientRequest -> { - ClientRequest authorizedRequest = ClientRequest.from(clientRequest) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue()) - .build(); - return Mono.just(authorizedRequest); - }); - } } diff --git a/samples/boot/oauth2login-webflux/src/main/resources/templates/index.html b/samples/boot/oauth2login-webflux/src/main/resources/templates/index.html index 19165ae1c3f..bbb2fab3f17 100644 --- a/samples/boot/oauth2login-webflux/src/main/resources/templates/index.html +++ b/samples/boot/oauth2login-webflux/src/main/resources/templates/index.html @@ -40,7 +40,12 @@

      OAuth 2.0 Login with Spring Security

       
      - Display User Info + User Attributes: +
        +
      • + : +
      • +
      diff --git a/samples/boot/oauth2login-webflux/src/main/resources/templates/userinfo.html b/samples/boot/oauth2login-webflux/src/main/resources/templates/userinfo.html deleted file mode 100644 index be4efb28001..00000000000 --- a/samples/boot/oauth2login-webflux/src/main/resources/templates/userinfo.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - Spring Security - OAuth 2.0 User Info - - - -
      -

      OAuth 2.0 User Info

      -
      - User Attributes: -
        -
      • - : -
      • -
      -
      - - diff --git a/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle b/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle index beee079bd41..01bce8a118f 100644 --- a/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle +++ b/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle @@ -6,11 +6,9 @@ dependencies { compile project(':spring-security-config') compile project(':spring-security-oauth2-client') compile project(':spring-security-oauth2-jose') - compile 'org.springframework:spring-webflux' compile 'org.springframework.boot:spring-boot-starter-thymeleaf' compile 'org.springframework.boot:spring-boot-starter-web' compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4' - compile 'io.projectreactor.ipc:reactor-netty' testCompile project(':spring-security-test') testCompile 'net.sourceforge.htmlunit:htmlunit' diff --git a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java index 1c722da290c..5bb0e81539a 100644 --- a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java +++ b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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,9 @@ import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; @@ -218,7 +220,7 @@ public void requestAuthorizationCodeGrantWhenNoMatchingAuthorizationRequestThenD page = this.webClient.getPage(new URL(authorizationResponseUri)); assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl); - HtmlElement errorElement = page.getBody().getFirstByXPath("p"); + HtmlElement errorElement = page.getBody().getFirstByXPath("div"); assertThat(errorElement).isNotNull(); assertThat(errorElement.asText()).contains("authorization_request_not_found"); } @@ -248,7 +250,7 @@ public void requestAuthorizationCodeGrantWhenInvalidStateParamThenDisplayLoginPa page = this.webClient.getPage(new URL(authorizationResponseUri)); assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl); - HtmlElement errorElement = page.getBody().getFirstByXPath("p"); + HtmlElement errorElement = page.getBody().getFirstByXPath("div"); assertThat(errorElement).isNotNull(); assertThat(errorElement.asText()).contains("authorization_request_not_found"); } @@ -284,13 +286,13 @@ public void requestAuthorizationCodeGrantWhenInvalidRedirectUriThenDisplayLoginP page = this.webClient.getPage(new URL(authorizationResponseUri)); assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl); - HtmlElement errorElement = page.getBody().getFirstByXPath("p"); + HtmlElement errorElement = page.getBody().getFirstByXPath("div"); assertThat(errorElement).isNotNull(); assertThat(errorElement.asText()).contains("invalid_redirect_uri_parameter"); } private void assertLoginPage(HtmlPage page) throws Exception { - assertThat(page.getTitleText()).isEqualTo("Login Page"); + assertThat(page.getTitleText()).isEqualTo("Please sign in"); int expectedClients = 4; @@ -403,12 +405,14 @@ private OAuth2UserService mockUserService() { @ComponentScan(basePackages = "sample.web") public static class SpringBootApplicationTestConfig { - @Autowired - private ClientRegistrationRepository clientRegistrationRepository; + @Bean + public OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) { + return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); + } @Bean - public OAuth2AuthorizedClientService authorizedClientService() { - return new InMemoryOAuth2AuthorizedClientService(this.clientRegistrationRepository); + public OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); } } } diff --git a/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java b/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java index 7ec72679feb..1682cfa0356 100644 --- a/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java +++ b/samples/boot/oauth2login/src/main/java/sample/OAuth2LoginApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -17,6 +17,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; /** * @author Joe Grandja @@ -24,11 +28,12 @@ @SpringBootApplication public class OAuth2LoginApplication { - public OAuth2LoginApplication() { - } - public static void main(String[] args) { SpringApplication.run(OAuth2LoginApplication.class, args); } + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); + } } diff --git a/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java b/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java index 4f8fa14ca82..10522edfff5 100644 --- a/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java +++ b/samples/boot/oauth2login/src/main/java/sample/web/OAuth2LoginController.java @@ -15,60 +15,28 @@ */ package sample.web; -import org.springframework.http.HttpHeaders; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.annotation.OAuth2Client; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.reactive.function.client.ClientRequest; -import org.springframework.web.reactive.function.client.ExchangeFilterFunction; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; - -import java.util.Collections; -import java.util.Map; /** * @author Joe Grandja + * @author Rob Winch */ @Controller public class OAuth2LoginController { @GetMapping("/") - public String index(Model model, @OAuth2Client OAuth2AuthorizedClient authorizedClient) { - model.addAttribute("userName", authorizedClient.getPrincipalName()); + public String index(Model model, + @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient, + @AuthenticationPrincipal OAuth2User oauth2User) { + model.addAttribute("userName", oauth2User.getName()); model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName()); + model.addAttribute("userAttributes", oauth2User.getAttributes()); return "index"; } - - @GetMapping("/userinfo") - public String userinfo(Model model, @OAuth2Client OAuth2AuthorizedClient authorizedClient) { - Map userAttributes = Collections.emptyMap(); - String userInfoEndpointUri = authorizedClient.getClientRegistration() - .getProviderDetails().getUserInfoEndpoint().getUri(); - if (!StringUtils.isEmpty(userInfoEndpointUri)) { // userInfoEndpointUri is optional for OIDC Clients - userAttributes = WebClient.builder() - .filter(oauth2Credentials(authorizedClient)) - .build() - .get() - .uri(userInfoEndpointUri) - .retrieve() - .bodyToMono(Map.class) - .block(); - } - model.addAttribute("userAttributes", userAttributes); - return "userinfo"; - } - - private ExchangeFilterFunction oauth2Credentials(OAuth2AuthorizedClient authorizedClient) { - return ExchangeFilterFunction.ofRequestProcessor( - clientRequest -> { - ClientRequest authorizedRequest = ClientRequest.from(clientRequest) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue()) - .build(); - return Mono.just(authorizedRequest); - }); - } } diff --git a/samples/boot/oauth2login/src/main/resources/templates/index.html b/samples/boot/oauth2login/src/main/resources/templates/index.html index c30b73de69c..c5a54504d8c 100644 --- a/samples/boot/oauth2login/src/main/resources/templates/index.html +++ b/samples/boot/oauth2login/src/main/resources/templates/index.html @@ -23,7 +23,12 @@

      OAuth 2.0 Login with Spring Security

       
      - Display User Info + User Attributes: +
        +
      • + : +
      • +
      diff --git a/samples/boot/oauth2login/src/main/resources/templates/userinfo.html b/samples/boot/oauth2login/src/main/resources/templates/userinfo.html deleted file mode 100644 index 2f7102469b6..00000000000 --- a/samples/boot/oauth2login/src/main/resources/templates/userinfo.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - Spring Security - OAuth 2.0 User Info - - - -
      -

      OAuth 2.0 User Info

      -
      - User Attributes: -
        -
      • - : -
      • -
      -
      - - diff --git a/samples/boot/oauth2resourceserver-webflux/README.adoc b/samples/boot/oauth2resourceserver-webflux/README.adoc new file mode 100644 index 00000000000..1960829a032 --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/README.adoc @@ -0,0 +1,169 @@ += OAuth 2.0 Resource Server Sample + +This sample demonstrates integrations with a handful of different authorization servers. + +With it, you can run the integration tests or run the application as a stand-alone service to explore how you can +secure your own service with OAuth 2.0 Bearer Tokens using Spring Security. + +== 1. Running the tests + +To run the tests, do: + +```bash +../../../gradlew integrationTest +``` + +Or import the project into your IDE and run `OAuth2ResourceServerApplicationTests` from there. + +=== What is it doing? + +By default, the tests are pointing at a demonstration Okta instance. The test that performs a valid round trip does so +by querying the Okta Authorization Server using the client_credentials grant type to get a valid JWT token. Then, the test +makes a query to the Resource Server with that token. The Resource Server subsquently verifies with Okta and +authorizes the request, returning the phrase + +```bash +Hello, {subject}! +``` + +where subject is the value of the `sub` field in the JWT returned by the Authorization Server. + +== 2. Running the app + +To run as a stand-alone application, do: + +```bash +../../../gradlew bootRun +``` + +Or import the project into your IDE and run `OAuth2ResourceServerApplication` from there. + +Once it is up, you can retreive a valid JWT token from the authorization server, and then hit the endpoint: + +```bash +curl -H "Authorization: Bearer {token}" localhost:8081 +``` + +Which will respond with the phrase: + +```bash +Hello, {subject}! +``` + +where `subject` is the value of the `sub` field in the JWT returned by the Authorization Server. + +=== How do I obtain a valid JWT token? + +Getting a valid JWT token from an Authorization Server will vary, depending on your setup. However, it will typically +look something like this: + +```bash +curl --user {client id}:{client password} -d "grant_type=client_credentials" {auth server endpoint}/token +``` + +which will respond with a JSON payload containing the `access_token` among other things: + +```bash +{ "access_token" : "{the access token}", "token_type" : "Bearer", "expires_in" : "{an expiry}", "scope" : "{a list of scopes}" } +``` + +For example, the following can be used to hit the sample Okta endpoint for a valid JWT token: + +```bash +curl --user 0oaf5u5g4m6CW4x6z0h7:HR7edRoo3glhF06HTxonOKZvO4I2BWYcC_ocOHlv -d "grant_type=client_credentials" https://dev-805262.oktapreview.com/oauth2/default/v1/token +``` + +Which will give a response similar to this (formatting mine): + +```json +{ + "access_token": "eyJraWQiOiJFRjBFWDFFWHZGc1hGaDhuYkRGazNJN0hMUDBsZnJnc0JKMVdBWmkwRmI0IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULmtQSUdfMEVMQmM3NVFMN3c4ZHBMVFRtNXZFVFd3d1R2dzJ3aXNISGRMbjgiLCJpc3MiOiJodHRwczovL2Rldi04MDUyNjIub2t0YXByZXZpZXcuY29tL29hdXRoMi9kZWZhdWx0IiwiYXVkIjoicmVzb3VyY2Utc2VydmVyIiwiaWF0IjoxNTI4ODYwMTkxLCJleHAiOjE1Mjg4NjM3OTEsImNpZCI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3Iiwic2NwIjpbIm9rIl0sInN1YiI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3In0.G_F9MQ3pqCy-YwfcNhryoPG5E1q4tQ7gV8OIDizR3QouUgrqT7MQsLQCTtGGLF2Fi0qq0Pr-V-wWa2MkyvcboEAhnfYi4rd3UmMrRTrNana6pVZjVWB_uj88-mZ57lFRnoYMCFbepmCxmY6D6p354H964xXWdtY7d6fw7F88DRDWMGQE0iQjMuUDg4izptVcK9db7uMonYTT1PFvOBQfwcn1zCeDVQgZFe7gjQA71CV9M6CIAXYDrpzp_hs95xco7Q3ncN3J7ZkCebLcUL6MdJS2nVuX6D6eC9PrtmCj06mb0-ydlzBSIUCPMaMQk9EhlEM_qK3d1iimCQnwo6KsIQ", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "ok" +} +``` + +Then, using that access token: + +```bash +curl -H "Authorization: Bearer eyJraWQiOiJFRjBFWDFFWHZGc1hGaDhuYkRGazNJN0hMUDBsZnJnc0JKMVdBWmkwRmI0IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULmtQSUdfMEVMQmM3NVFMN3c4ZHBMVFRtNXZFVFd3d1R2dzJ3aXNISGRMbjgiLCJpc3MiOiJodHRwczovL2Rldi04MDUyNjIub2t0YXByZXZpZXcuY29tL29hdXRoMi9kZWZhdWx0IiwiYXVkIjoicmVzb3VyY2Utc2VydmVyIiwiaWF0IjoxNTI4ODYwMTkxLCJleHAiOjE1Mjg4NjM3OTEsImNpZCI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3Iiwic2NwIjpbIm9rIl0sInN1YiI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3In0.G_F9MQ3pqCy-YwfcNhryoPG5E1q4tQ7gV8OIDizR3QouUgrqT7MQsLQCTtGGLF2Fi0qq0Pr-V-wWa2MkyvcboEAhnfYi4rd3UmMrRTrNana6pVZjVWB_uj88-mZ57lFRnoYMCFbepmCxmY6D6p354H964xXWdtY7d6fw7F88DRDWMGQE0iQjMuUDg4izptVcK9db7uMonYTT1PFvOBQfwcn1zCeDVQgZFe7gjQA71CV9M6CIAXYDrpzp_hs95xco7Q3ncN3J7ZkCebLcUL6MdJS2nVuX6D6eC9PrtmCj06mb0-ydlzBSIUCPMaMQk9EhlEM_qK3d1iimCQnwo6KsIQ" \ + localhost:8081 +``` + +I get: + +```bash +Hello, 0oaf5u5g4m6CW4x6z0h7! +``` + +== 3. Testing against other Authorization Servers + +The sample is already prepared to demonstrate integrations with a handful of other Authorization Servers. Do exercise +one, simply uncomment two commented out sections, both in the application.yml file: + +```yaml +spring: + security: + oauth2: + resourceserver: + issuer: +``` + +First, find the above section in the application.yml. Beneath it, you will see sections for each Authorization Server +already prepared with the one for Okta commented out: + +```yaml +# master: #keycloak +# issuer: http://localhost:8080/auth/realms/master +# jwk-set-uri: http://localhost:8080/auth/realms/master/protocol/openid-connect/certs + okta: + issuer: https://dev-805262.oktapreview.com/oauth2/default + jwk-set-uri: https://dev-805262.oktapreview.com/oauth2/default/v1/keys +``` + +Comment out the `okta` section and uncomment the desired section. + +Second, find the following section, which the sample needs in order to retreive a valid token from the Authorization +Server: + +```yaml +# ### keycloak +# token-uri: http://localhost:8080/auth/realms/master/protocol/openid-connect/token +# token-body: +# grant_type: client_credentials +# client-id: service +# client-password: 9114712b-be55-4dab-b270-04734abda1c4 +# container: +# config-file-name: keycloak.config +# docker-file-name: keycloak.docker + ### okta + token-uri: https://dev-805262.oktapreview.com/oauth2/default/v1/token + token-body: + grant_type: client_credentials + client-id: 0oaf5u5g4m6CW4x6z0h7 + client-password: HR7edRoo3glhF06HTxonOKZvO4I2BWYcC_ocOHlv +``` + +Comment out the `okta` section and uncomment the desired section. + +=== How can I test with my own Authorization Server instance? + +To test with your own Okta or other Authorization Server instance, simply provide the following information: + +```yaml +spring.security.oauth2.resourceserver.issuer.name.uri: the issuer uri +spring.security.oauth2.resourceserver.issuer.name.jwk-set-uri: the jwk key uri +``` + +And indicate, using the sample.provider properties, how the sample should generate a valid JWT token: + +```yaml +sample.provider.token-uri: the token endpoint +sample.provider.token-body.grant_type: the grant to use +sample.provider.token-body.another_property: another_value +sample.provider.client-id: the client id +sample.provider.client-password: the client password, only required for confidential clients +``` + +You can provide values for any OAuth 2.0-compliant Authorization Server. diff --git a/samples/boot/oauth2resourceserver-webflux/spring-security-samples-boot-oauth2resourceserver-webflux.gradle b/samples/boot/oauth2resourceserver-webflux/spring-security-samples-boot-oauth2resourceserver-webflux.gradle new file mode 100644 index 00000000000..cf0d0d6202b --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/spring-security-samples-boot-oauth2resourceserver-webflux.gradle @@ -0,0 +1,15 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-security-config') + compile project(':spring-security-oauth2-jose') + compile project(':spring-security-oauth2-client') + compile project(':spring-security-oauth2-resource-server') + + compile 'org.springframework.boot:spring-boot-starter-webflux' + + testCompile project(':spring-security-test') + testCompile 'org.springframework.boot:spring-boot-starter-test' + + testCompile 'com.squareup.okhttp3:mockwebserver' +} diff --git a/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/OAuth2ResourceServerController.java b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/OAuth2ResourceServerController.java new file mode 100644 index 00000000000..7024f39f909 --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/OAuth2ResourceServerController.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 sample; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Josh Cummings + */ +@RestController +public class OAuth2ResourceServerController { + + @GetMapping("/") + public String index(@AuthenticationPrincipal Jwt jwt) { + return String.format("Hello, %s!", jwt.getSubject()); + } +} diff --git a/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java new file mode 100644 index 00000000000..f8ce3b3de0f --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 sample; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; + +/** + * @author Rob Winch + * @since 5.1 + */ +@EnableWebFluxSecurity +public class SecurityConfig { + private static final String JWK_SET_URI_PROP = "sample.jwk-set-uri"; + + @Bean + @ConditionalOnProperty(SecurityConfig.JWK_SET_URI_PROP) + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, @Value("${sample.jwk-set-uri}") String jwkSetUri) throws Exception { + http + .authorizeExchange() + .anyExchange().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(jwkSetUri); + return http.build(); + } + + @Bean + @ConditionalOnProperty(matchIfMissing = true, name = SecurityConfig.JWK_SET_URI_PROP) + SecurityWebFilterChain springSecurityFilterChainWithJwkSetUri(ServerHttpSecurity http) throws Exception { + http + .authorizeExchange() + .anyExchange().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .publicKey(publicKey()); + return http.build(); + } + + private RSAPublicKey publicKey() + throws NoSuchAlgorithmException, InvalidKeySpecException { + String modulus = "21301844740604653578042500449274548398885553541276518010855123403873267398204269788903348794459771698460057967144865511347818036788093430902099139850950702438493841101242291810362822203615900335437741117578551216365305797763072813300890517195382010982402736091906390325356368590938709762826676219814134995844721978269999358693499223506089799649124650473473086179730568497569430199548044603025675755473148289824338392487941265829853008714754732175256733090080910187256164496297277607612684421019218165083081805792835073696987599616469568512360535527950859101589894643349122454163864596223876828010734083744763850611111"; + String exponent = "65537"; + + RSAPublicKeySpec spec = new RSAPublicKeySpec(new BigInteger(modulus), new BigInteger(exponent)); + KeyFactory factory = KeyFactory.getInstance("RSA"); + return (RSAPublicKey) factory.generatePublic(spec); + } +} diff --git a/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/ServerOAuth2ResourceServerApplication.java b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/ServerOAuth2ResourceServerApplication.java new file mode 100644 index 00000000000..3061103fbcf --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/ServerOAuth2ResourceServerApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Rob Winch + */ +@SpringBootApplication +public class ServerOAuth2ResourceServerApplication { + + public static void main(String[] args) { + SpringApplication.run(ServerOAuth2ResourceServerApplication.class, args); + } +} diff --git a/samples/boot/oauth2resourceserver-webflux/src/main/resources/application.yml b/samples/boot/oauth2resourceserver-webflux/src/main/resources/application.yml new file mode 100644 index 00000000000..cf18ae712d2 --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/src/main/resources/application.yml @@ -0,0 +1,11 @@ +logging: + level: + root: INFO + org.springframework.web: INFO + org.springframework.security: INFO +# org.springframework.boot.autoconfigure: DEBUG + +sample: +# By default this sample uses a hard coded public key in SecurityConfig +# To use a JWK Set URI, uncomment and change the value below +# jwk-set-uri: https://example.com/oauth2/default/v1/keys diff --git a/samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java b/samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java new file mode 100644 index 00000000000..a47be4787e6 --- /dev/null +++ b/samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 sample; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author Rob Winch + * @since 5.1 + */ +@SpringBootTest +@AutoConfigureWebTestClient +@RunWith(SpringJUnit4ClassRunner.class) +public class ServerOauth2ResourceApplicationTests { + @Autowired + private WebTestClient rest; + + @Test + public void getWhenValidTokenThenIsOk() { + String token = "eyJhbGciOiJSUzI1NiJ9.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6MzEwNjMyODEzMSwianRpIjoiOGY5ZjFiYzItOWVlMi00NTJkLThhMGEtODg3YmE4YmViYjYzIn0.CM_KulSsIrNXW1x6NFeN5VwKQiIW-LIAScJzakRFDox8Ql7o4WOb0ubY3CjWYnglwqYzBvH9McCFqVrUtzdfODY5tyEEJSxWndIGExOi2osrwRPsY3AGzNa23GMfC9I03BFP1IFCq4ZfL-L6yVcIjLke-rA40UG-r-oA7r-N_zsLc5poO7Azf29IQgQF0GSRp4AKQprYHF5Q-Nz9XkILMDz9CwPQ9cbdLCC9smvaGmEAjMUr-C1QgM-_ulb42gWtRDLorW_eArg8g-fmIP0_w82eNWCBjLTy-WaDMACnDVrrUVsUMCqx6jS6h8_uejKly2NFuhyueIHZTTySqCZoTA"; + this.rest.get().uri("/") + .headers(headers -> headers.setBearerAuth(token)) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello, null!"); + } + + @Test + public void getWhenNoTokenThenIsUnauthorized() { + this.rest.get().uri("/") + .exchange() + .expectStatus().isUnauthorized() + .expectHeader().valueEquals(HttpHeaders.WWW_AUTHENTICATE, "Bearer"); + } + + @Test + public void getWhenNone() { + String token = "ew0KICAiYWxnIjogIm5vbmUiLA0KICAidHlwIjogIkpXVCINCn0.ew0KICAic3ViIjogIjEyMzQ1Njc4OTAiLA0KICAibmFtZSI6ICJKb2huIERvZSIsDQogICJpYXQiOiAxNTE2MjM5MDIyDQp9."; + this.rest.get().uri("/") + .headers(headers -> headers.setBearerAuth(token)) + .exchange() + .expectStatus().isUnauthorized() + .expectHeader().valueEquals(HttpHeaders.WWW_AUTHENTICATE, "Bearer error=\"invalid_token\", error_description=\"Unsupported algorithm of none\", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } + + @Test + public void getWhenInvalidToken() { + String token = "a"; + this.rest.get().uri("/") + .headers(headers -> headers.setBearerAuth(token)) + .exchange() + .expectStatus().isUnauthorized() + .expectHeader().valueEquals(HttpHeaders.WWW_AUTHENTICATE, "Bearer error=\"invalid_token\", error_description=\"An error occurred while attempting to decode the Jwt: Invalid JWT serialization: Missing dot delimiter(s)\", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } +} diff --git a/samples/boot/oauth2resourceserver/README.adoc b/samples/boot/oauth2resourceserver/README.adoc new file mode 100644 index 00000000000..d3ba0b8238c --- /dev/null +++ b/samples/boot/oauth2resourceserver/README.adoc @@ -0,0 +1,104 @@ += OAuth 2.0 Resource Server Sample + +This sample demonstrates integrating Resource Server with a mock Authorization Server, though it can be modified to integrate +with your favorite Authorization Server. + +With it, you can run the integration tests or run the application as a stand-alone service to explore how you can +secure your own service with OAuth 2.0 Bearer Tokens using Spring Security. + +== 1. Running the tests + +To run the tests, do: + +```bash +./gradlew integrationTest +``` + +Or import the project into your IDE and run `OAuth2ResourceServerApplicationTests` from there. + +=== What is it doing? + +By default, the tests are pointing at a mock Authorization Server instance. + +The tests are configured with a set of hard-coded tokens originally obtained from the mock Authorization Server, +and each makes a query to the Resource Server with their corresponding token. + +The Resource Server subsquently verifies with the Authorization Server and authorizes the request, returning the phrase + +```bash +Hello, subject! +``` + +where "subject" is the value of the `sub` field in the JWT returned by the Authorization Server. + +== 2. Running the app + +To run as a stand-alone application, do: + +```bash +./gradlew bootRun +``` + +Or import the project into your IDE and run `OAuth2ResourceServerApplication` from there. + +Once it is up, you can use the following token: + +```bash +export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ.ULEPdHG-MK5GlrTQMhgqcyug2brTIZaJIrahUeq9zaiwUSdW83fJ7W1IDd2Z3n4a25JY2uhEcoV95lMfccHR6y_2DLrNvfta22SumY9PEDF2pido54LXG6edIGgarnUbJdR4rpRe_5oRGVa8gDx8FnuZsNv6StSZHAzw5OsuevSTJ1UbJm4UfX3wiahFOQ2OI6G-r5TB2rQNdiPHuNyzG5yznUqRIZ7-GCoMqHMaC-1epKxiX8gYXRROuUYTtcMNa86wh7OVDmvwVmFioRcR58UWBRoO1XQexTtOQq_t8KYsrPZhb9gkyW8x2bAQF-d0J0EJY8JslaH6n4RBaZISww +``` + +And then make this request: + +```bash +curl -H "Authorization: Bearer $TOKEN" localhost:8080 +``` + +Which will respond with the phrase: + +```bash +Hello, subject! +``` + +where `subject` is the value of the `sub` field in the JWT returned by the Authorization Server. + +Or this: + +```bash +export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A + +curl -H "Authorization: Bearer $TOKEN" localhost:8080/message +``` + +Will respond with: + +```bash +secret message +``` + +== 2. Testing against other Authorization Servers + +_In order to use this sample, your Authorization Server must support JWTs that either use the "scope" or "scp" attribute._ + +_Additionally, remember that if your authorization server is running locally on port 8080, you'll need to change the sample's port in the `application.yml` by adding something like `server.port: 8082`._ + +To change the sample to point at your Authorization Server, simply find this property in the `application.yml`: + +```yaml +sample.jwk-set-uri: mock://localhost:8081/.well-known/jwks.json +``` + +And change the property to your Authorization Server's JWK set endpoint: + +```yaml +sample.jwk-set-uri: https://dev-123456.oktapreview.com/oauth2/default/v1/keys +``` + +And then you can run the app the same as before: + +```bash +./gradlew bootRun +``` + +Make sure to obtain valid tokens from your Authorization Server in order to play with the sample Resource Server. +To use the `/` endpoint, any valid token from your Authorization Server will do. +To use the `/message` endpoint, the token should have the `message:read` scope. diff --git a/samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle b/samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle new file mode 100644 index 00000000000..2135bb0af66 --- /dev/null +++ b/samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle @@ -0,0 +1,13 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-security-config') + compile project(':spring-security-oauth2-jose') + compile project(':spring-security-oauth2-resource-server') + + compile 'org.springframework.boot:spring-boot-starter-web' + compile 'com.squareup.okhttp3:mockwebserver' + + testCompile project(':spring-security-test') + testCompile 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/samples/boot/oauth2resourceserver/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java b/samples/boot/oauth2resourceserver/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java new file mode 100644 index 00000000000..19788620676 --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java @@ -0,0 +1,101 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://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 sample; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import static org.hamcrest.Matchers.containsString; +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; + +/** + * Integration tests for {@link OAuth2ResourceServerApplication} + * + * @author Josh Cummings + */ +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +public class OAuth2ResourceServerApplicationITests { + + String noScopesToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ.ULEPdHG-MK5GlrTQMhgqcyug2brTIZaJIrahUeq9zaiwUSdW83fJ7W1IDd2Z3n4a25JY2uhEcoV95lMfccHR6y_2DLrNvfta22SumY9PEDF2pido54LXG6edIGgarnUbJdR4rpRe_5oRGVa8gDx8FnuZsNv6StSZHAzw5OsuevSTJ1UbJm4UfX3wiahFOQ2OI6G-r5TB2rQNdiPHuNyzG5yznUqRIZ7-GCoMqHMaC-1epKxiX8gYXRROuUYTtcMNa86wh7OVDmvwVmFioRcR58UWBRoO1XQexTtOQq_t8KYsrPZhb9gkyW8x2bAQF-d0J0EJY8JslaH6n4RBaZISww"; + String messageReadToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A"; + + @Autowired + MockMvc mvc; + + @Test + public void performWhenValidBearerTokenThenAllows() + throws Exception { + + this.mvc.perform(get("/").with(bearerToken(this.noScopesToken))) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Hello, subject!"))); + } + + // -- tests with scopes + + @Test + public void performWhenValidBearerTokenThenScopedRequestsAlsoWork() + throws Exception { + + this.mvc.perform(get("/message").with(bearerToken(this.messageReadToken))) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("secret message"))); + } + + @Test + public void performWhenInsufficientlyScopedBearerTokenThenDeniesScopedMethodAccess() + throws Exception { + + this.mvc.perform(get("/message").with(bearerToken(this.noScopesToken))) + .andExpect(status().isForbidden()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, + containsString("Bearer error=\"insufficient_scope\""))); + } + + private static class BearerTokenRequestPostProcessor implements RequestPostProcessor { + private String token; + + public BearerTokenRequestPostProcessor(String token) { + this.token = token; + } + + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + request.addHeader("Authorization", "Bearer " + this.token); + return request; + } + } + + private static BearerTokenRequestPostProcessor bearerToken(String token) { + return new BearerTokenRequestPostProcessor(token); + } +} diff --git a/samples/boot/oauth2resourceserver/src/integration-test/resources/application-test.yml b/samples/boot/oauth2resourceserver/src/integration-test/resources/application-test.yml new file mode 100644 index 00000000000..04878b289c8 --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/integration-test/resources/application-test.yml @@ -0,0 +1 @@ +sample.jwk-set-uri: mock://localhost:0/.well-known/jwks.json diff --git a/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerApplication.java b/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerApplication.java new file mode 100644 index 00000000000..f9cc432b1b3 --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Josh Cummings + */ +@SpringBootApplication +public class OAuth2ResourceServerApplication { + + public static void main(String[] args) { + SpringApplication.run(OAuth2ResourceServerApplication.class, args); + } +} diff --git a/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerController.java b/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerController.java new file mode 100644 index 00000000000..9cb92c21d6a --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerController.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 sample; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Josh Cummings + */ +@RestController +public class OAuth2ResourceServerController { + + @GetMapping("/") + public String index(@AuthenticationPrincipal Jwt jwt) { + return String.format("Hello, %s!", jwt.getSubject()); + } + + @GetMapping("/message") + public String message() { + return "secret message"; + } +} diff --git a/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java b/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java new file mode 100644 index 00000000000..91a44c72239 --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 sample; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +/** + * @author Josh Cummings + */ +@EnableWebSecurity +public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter { + @Value("${sample.jwk-set-uri}") + String jwkSetUri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .antMatchers("/message/**").access("hasAuthority('SCOPE_message:read')") + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .jwkSetUri(this.jwkSetUri); + // @formatter:on + } +} diff --git a/samples/boot/oauth2resourceserver/src/main/java/sample/provider/MockProvider.java b/samples/boot/oauth2resourceserver/src/main/java/sample/provider/MockProvider.java new file mode 100644 index 00000000000..b3e16318ece --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/main/java/sample/provider/MockProvider.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 sample.provider; + +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.PreDestroy; + +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +/** + * This is a miminal mock server that serves as a placeholder for a real Authorization Server (AS). + * + * For the sample to work, the AS used must support a JWK endpoint. + * + * For the integration tests to work, the AS used must be able to issue a token + * with the following characteristics: + * + * - The token has the "message:read" scope + * - The token has a "sub" of "subject" + * - The token is signed by a RS256 private key whose public key counterpart is served from the JWK endpoint of the AS. + * + * There is also a test that verifies insufficient scope. In that case, the token should have the following characteristics: + * + * - The token is missing the "message:read" scope + * - The token is signed by a RS256 private key whose public key counterpart is served from the JWK endpoint of the AS. + * + * @author Josh Cummings + */ +public class MockProvider implements EnvironmentPostProcessor { + private MockWebServer server = new MockWebServer(); + + private static final MockResponse JWKS_RESPONSE = response( + "{\"keys\":[{\"p\":\"2p-ViY7DE9ZrdWQb544m0Jp7Cv03YCSljqfim9pD4ALhObX0OrAznOiowTjwBky9JGffMwDBVSfJSD9TSU7aH2sbbfi0bZLMdekKAuimudXwUqPDxrrg0BCyvCYgLmKjbVT3zcdylWSog93CNTxGDPzauu-oc0XPNKCXnaDpNvE\",\"kty\":\"RSA\",\"q\":\"sP_QYavrpBvSJ86uoKVGj2AGl78CSsAtpf1ybSY5TwUlorXSdqapRbY69Y271b0aMLzlleUn9ZTBO1dlKV2_dw_lPADHVia8z3pxL-8sUhIXLsgj4acchMk4c9YX-sFh07xENnyZ-_TXm3llPLuL67HUfBC2eKe800TmCYVWc9U\",\"d\":\"bn1nFxCQT4KLTHqo8mo9HvHD0cRNRNdWcKNnnEQkCF6tKbt-ILRyQGP8O40axLd7CoNVG9c9p_-g4-2kwCtLJNv_STLtwfpCY7VN5o6-ZIpfTjiW6duoPrLWq64Hm_4LOBQTiZfUPcLhsuJRHbWqakj-kV_YbUyC2Ocf_dd8IAQcSrAU2SCcDebhDCWwRUFvaa9V5eq0851S9goaA-AJz-JXyePH6ZFr8JxmWkWxYZ5kdcMD-sm9ZbxE0CaEk32l4fE4hR-L8x2dDtjWA-ahKCZ091z-gV3HWtR2JOjvxoNRjxUo3UxaGiFJHWNIl0EYUJZu1Cb-5wIlEI7wPx5mwQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"qS0OK48M2CIAA6_4Wdw4EbCaAfcTLf5Oy9t5BOF_PFUKqoSpZ6JsT5H0a_4zkjt-oI969v78OTlvBKbmEyKO-KeytzHBAA5CsLmVcz0THrMSg6oXZqu66MPnvWoZN9FEN5TklPOvBFm8Bg1QZ3k-YMVaM--DLvhaYR95_mqaz50\",\"dp\":\"Too2NozLGD1XrXyhabZvy1E0EuaVFj0UHQPDLSpkZ_2g3BK6Art6T0xmE8RYtmqrKIEIdlI3IliAvyvAx_1D7zWTTRaj-xlZyqJFrnXWL7zj8UxT8PkB-r2E-ILZ3NAi1gxIWezlBTZ8M6NfObDFmbTc_3tJkN_raISo8z_ziIE\",\"dq\":\"U0yhSkY5yOsa9YcMoigGVBWSJLpNHtbg5NypjHrPv8OhWbkOSq7WvSstBkFk5AtyFvvfZLMLIkWWxxGzV0t6f1MoxBtttLrYYyCxwihiiGFhLbAdSuZ1wnxcqA9bC7UVECvrQmVTpsMs8UupfHKbQBpZ8OWAqrnuYNNtG4_4Bt0\",\"n\":\"lygtuZj0lJjqOqIWocF8Bb583QDdq-aaFg8PesOp2-EDda6GqCpL-_NZVOflNGX7XIgjsWHcPsQHsV9gWuOzSJ0iEuWvtQ6eGBP5M6m7pccLNZfwUse8Cb4Ngx3XiTlyuqM7pv0LPyppZusfEHVEdeelou7Dy9k0OQ_nJTI3b2E1WBoHC58CJ453lo4gcBm1efURN3LIVc1V9NQY_ESBKVdwqYyoJPEanURLVGRd6cQKn6YrCbbIRHjqAyqOE-z3KmgDJnPriljfR5XhSGyM9eqD9Xpy6zu_MAeMJJfSArp857zLPk-Wf5VP9STAcjyfdBIybMKnwBYr2qHMT675hQ\"}]}", + 200 + ); + + private static final MockResponse NOT_FOUND_RESPONSE = response( + "{ \"message\" : \"This mock authorization server responds to just one request: GET /.well-known/jwks.json.\" }", + 404 + ); + + public MockProvider() throws IOException { + Dispatcher dispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + if ("/.well-known/jwks.json".equals(request.getPath())) { + return JWKS_RESPONSE; + } + + return NOT_FOUND_RESPONSE; + } + }; + + this.server.setDispatcher(dispatcher); + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + String uri = environment.getProperty("sample.jwk-set-uri", "mock://localhost:0"); + + if (uri.startsWith("mock://")) { + try { + this.server.start(URI.create(uri).getPort()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + Map properties = new HashMap<>(); + String url = this.server.url("/.well-known/jwks.json").toString(); + properties.put("sample.jwk-set-uri", url); + + MapPropertySource propertySource = new MapPropertySource("mock", properties); + environment.getPropertySources().addFirst(propertySource); + } + } + + @PreDestroy + public void shutdown() throws IOException { + this.server.shutdown(); + } + + private static MockResponse response(String body, int status) { + return new MockResponse() + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .setResponseCode(status) + .setBody(body); + } +} diff --git a/samples/boot/oauth2resourceserver/src/main/resources/META-INF/spring.factories b/samples/boot/oauth2resourceserver/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..34562aa3b0e --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.env.EnvironmentPostProcessor=sample.provider.MockProvider diff --git a/samples/boot/oauth2resourceserver/src/main/resources/application.yml b/samples/boot/oauth2resourceserver/src/main/resources/application.yml new file mode 100644 index 00000000000..f61da202dfa --- /dev/null +++ b/samples/boot/oauth2resourceserver/src/main/resources/application.yml @@ -0,0 +1 @@ +sample.jwk-set-uri: mock://localhost:8081/.well-known/jwks.json diff --git a/samples/javaconfig/webflux-form/spring-security-samples-javaconfig-webflux-form.gradle b/samples/boot/webflux-form/spring-security-samples-boot-webflux-form.gradle similarity index 74% rename from samples/javaconfig/webflux-form/spring-security-samples-javaconfig-webflux-form.gradle rename to samples/boot/webflux-form/spring-security-samples-boot-webflux-form.gradle index cf489639314..f42a169c657 100644 --- a/samples/javaconfig/webflux-form/spring-security-samples-javaconfig-webflux-form.gradle +++ b/samples/boot/webflux-form/spring-security-samples-boot-webflux-form.gradle @@ -14,24 +14,21 @@ * limitations under the License. */ -apply plugin: 'io.spring.convention.spring-sample' +apply plugin: 'io.spring.convention.spring-sample-boot' dependencies { compile project(':spring-security-core') compile project(':spring-security-config') compile project(':spring-security-web') - compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'io.netty:netty-buffer' - compile 'io.projectreactor.ipc:reactor-netty' - compile 'org.springframework:spring-context' - compile 'org.springframework:spring-webflux' - compile 'org.thymeleaf:thymeleaf-spring5' - compile slf4jDependencies + compile 'org.springframework.boot:spring-boot-starter-thymeleaf' + compile 'org.springframework.boot:spring-boot-starter-webflux' testCompile project(':spring-security-test') + testCompile 'org.springframework.boot:spring-boot-starter-test' testCompile 'io.projectreactor:reactor-test' testCompile 'org.skyscreamer:jsonassert' testCompile 'org.springframework:spring-test' integrationTestCompile seleniumDependencies } + diff --git a/samples/javaconfig/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java b/samples/boot/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java similarity index 84% rename from samples/javaconfig/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java rename to samples/boot/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java index 7e6e1373452..b8bf628bc37 100644 --- a/samples/javaconfig/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java +++ b/samples/boot/webflux-form/src/integration-test/java/sample/WebfluxFormApplicationTests.java @@ -15,16 +15,17 @@ */ package sample; -import com.gargoylesoftware.htmlunit.BrowserVersion; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.openqa.selenium.WebDriver; import org.openqa.selenium.htmlunit.HtmlUnitDriver; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; import org.springframework.test.context.junit4.SpringRunner; + +import com.gargoylesoftware.htmlunit.BrowserVersion; + import sample.webdriver.IndexPage; import sample.webdriver.LoginPage; @@ -33,12 +34,11 @@ * @since 5.0 */ @RunWith(SpringRunner.class) -@ContextConfiguration(classes = WebfluxFormApplication.class) -@TestPropertySource(properties = "server.port=0") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WebfluxFormApplicationTests { WebDriver driver; - @Value("#{@nettyContext.address().getPort()}") + @LocalServerPort int port; @Before diff --git a/samples/javaconfig/webflux-form/src/integration-test/java/sample/webdriver/IndexPage.java b/samples/boot/webflux-form/src/integration-test/java/sample/webdriver/IndexPage.java similarity index 100% rename from samples/javaconfig/webflux-form/src/integration-test/java/sample/webdriver/IndexPage.java rename to samples/boot/webflux-form/src/integration-test/java/sample/webdriver/IndexPage.java diff --git a/samples/javaconfig/webflux-form/src/integration-test/java/sample/webdriver/LoginPage.java b/samples/boot/webflux-form/src/integration-test/java/sample/webdriver/LoginPage.java similarity index 100% rename from samples/javaconfig/webflux-form/src/integration-test/java/sample/webdriver/LoginPage.java rename to samples/boot/webflux-form/src/integration-test/java/sample/webdriver/LoginPage.java diff --git a/samples/javaconfig/webflux-form/src/main/java/sample/CsrfControllerAdvice.java b/samples/boot/webflux-form/src/main/java/sample/CsrfControllerAdvice.java similarity index 100% rename from samples/javaconfig/webflux-form/src/main/java/sample/CsrfControllerAdvice.java rename to samples/boot/webflux-form/src/main/java/sample/CsrfControllerAdvice.java diff --git a/samples/javaconfig/webflux-form/src/main/java/sample/IndexController.java b/samples/boot/webflux-form/src/main/java/sample/IndexController.java similarity index 100% rename from samples/javaconfig/webflux-form/src/main/java/sample/IndexController.java rename to samples/boot/webflux-form/src/main/java/sample/IndexController.java diff --git a/samples/boot/webflux-form/src/main/java/sample/WebfluxFormApplication.java b/samples/boot/webflux-form/src/main/java/sample/WebfluxFormApplication.java new file mode 100644 index 00000000000..9b93f1e9d48 --- /dev/null +++ b/samples/boot/webflux-form/src/main/java/sample/WebfluxFormApplication.java @@ -0,0 +1,32 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://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 sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Rob Winch + * @since 5.0 + */ +@SpringBootApplication +public class WebfluxFormApplication { + + public static void main(String[] args) { + SpringApplication.run(WebfluxFormApplication.class, args); + } +} diff --git a/samples/javaconfig/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java b/samples/boot/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java similarity index 100% rename from samples/javaconfig/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java rename to samples/boot/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java diff --git a/samples/javaconfig/webflux-form/src/main/resources/logback.xml b/samples/boot/webflux-form/src/main/resources/logback.xml similarity index 100% rename from samples/javaconfig/webflux-form/src/main/resources/logback.xml rename to samples/boot/webflux-form/src/main/resources/logback.xml diff --git a/samples/javaconfig/webflux-form/src/main/resources/templates/index.html b/samples/boot/webflux-form/src/main/resources/templates/index.html similarity index 100% rename from samples/javaconfig/webflux-form/src/main/resources/templates/index.html rename to samples/boot/webflux-form/src/main/resources/templates/index.html diff --git a/samples/javaconfig/webflux-form/src/main/resources/templates/login.html b/samples/boot/webflux-form/src/main/resources/templates/login.html similarity index 100% rename from samples/javaconfig/webflux-form/src/main/resources/templates/login.html rename to samples/boot/webflux-form/src/main/resources/templates/login.html diff --git a/samples/javaconfig/hellowebflux-method/spring-security-samples-javaconfig-hellowebflux-method.gradle b/samples/javaconfig/hellowebflux-method/spring-security-samples-javaconfig-hellowebflux-method.gradle deleted file mode 100644 index 33375a6fb71..00000000000 --- a/samples/javaconfig/hellowebflux-method/spring-security-samples-javaconfig-hellowebflux-method.gradle +++ /dev/null @@ -1,16 +0,0 @@ -apply plugin: 'io.spring.convention.spring-sample' - -dependencies { - compile project(':spring-security-core') - compile project(':spring-security-config') - compile project(':spring-security-web') - compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'io.projectreactor.ipc:reactor-netty' - compile 'org.springframework:spring-context' - compile 'org.springframework:spring-webflux' - - testCompile project(':spring-security-test') - testCompile 'io.projectreactor:reactor-test' - testCompile 'org.skyscreamer:jsonassert' - testCompile 'org.springframework:spring-test' -} diff --git a/samples/javaconfig/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java b/samples/javaconfig/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java deleted file mode 100644 index 813614f4bf5..00000000000 --- a/samples/javaconfig/hellowebflux-method/src/integration-test/java/sample/HelloWebfluxMethodApplicationITests.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://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 sample; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.reactive.server.WebTestClient; - -import java.nio.charset.Charset; -import java.time.Duration; -import java.util.Base64; -import java.util.Map; -import java.util.function.Consumer; - -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; - -/** - * @author Rob Winch - * @since 5.0 - */ -@RunWith(SpringRunner.class) -@ContextConfiguration(classes = HelloWebfluxMethodApplication.class) -@TestPropertySource(properties = "server.port=0") -public class HelloWebfluxMethodApplicationITests { - @Value("#{@nettyContext.address().getPort()}") - int port; - - WebTestClient rest; - - @Before - public void setup() { - this.rest = WebTestClient.bindToServer() - .filter(basicAuthentication()) - .responseTimeout(Duration.ofDays(1)) - .baseUrl("http://localhost:" + this.port) - .build(); - } - - @Test - public void messageWhenNotAuthenticated() throws Exception { - this.rest - .get() - .uri("/message") - .exchange() - .expectStatus().isUnauthorized(); - } - - @Test - public void messageWhenUserThenForbidden() throws Exception { - this.rest - .get() - .uri("/message") - .attributes(robsCredentials()) - .exchange() - .expectStatus().isEqualTo(HttpStatus.FORBIDDEN); - } - - @Test - public void messageWhenAdminThenOk() throws Exception { - this.rest - .get() - .uri("/message") - .attributes(adminCredentials()) - .exchange() - .expectStatus().isOk() - .expectBody(String.class).isEqualTo("Hello World!"); - } - - private Consumer> robsCredentials() { - return basicAuthenticationCredentials("rob", "rob"); - } - - private Consumer> adminCredentials() { - return basicAuthenticationCredentials("admin", "admin"); - } - - private String base64Encode(String value) { - return Base64.getEncoder().encodeToString(value.getBytes(Charset.defaultCharset())); - } -} diff --git a/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java b/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java deleted file mode 100644 index 0d068142d84..00000000000 --- a/samples/javaconfig/hellowebflux-method/src/main/java/sample/HelloWebfluxMethodApplication.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://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 sample; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.*; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import reactor.ipc.netty.NettyContext; -import reactor.ipc.netty.http.server.HttpServer; - -/** - * @author Rob Winch - * @since 5.0 - */ -@Configuration -@EnableWebFlux -@ComponentScan -public class HelloWebfluxMethodApplication { - @Value("${server.port:8080}") - private int port = 8080; - - public static void main(String[] args) throws Exception { - try(AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - HelloWebfluxMethodApplication.class)) { - context.getBean(NettyContext.class).onClose().block(); - } - } - - @Profile("default") - @Bean - public NettyContext nettyContext(ApplicationContext context) { - HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context) - .build(); - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); - HttpServer httpServer = HttpServer.create("localhost", port); - return httpServer.newHandler(adapter).block(); - } -} diff --git a/samples/javaconfig/hellowebflux/spring-security-samples-javaconfig-hellowebflux.gradle b/samples/javaconfig/hellowebflux/spring-security-samples-javaconfig-hellowebflux.gradle deleted file mode 100644 index 5ac19deea82..00000000000 --- a/samples/javaconfig/hellowebflux/spring-security-samples-javaconfig-hellowebflux.gradle +++ /dev/null @@ -1,17 +0,0 @@ -apply plugin: 'io.spring.convention.spring-sample' - -dependencies { - compile project(':spring-security-core') - compile project(':spring-security-config') - compile project(':spring-security-web') - compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'io.projectreactor.ipc:reactor-netty' - compile 'org.springframework:spring-context' - compile 'org.springframework:spring-webflux' - compile slf4jDependencies - - testCompile project(':spring-security-test') - testCompile 'io.projectreactor:reactor-test' - testCompile 'org.skyscreamer:jsonassert' - testCompile 'org.springframework:spring-test' -} diff --git a/samples/javaconfig/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java b/samples/javaconfig/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java deleted file mode 100644 index fd2db5edb84..00000000000 --- a/samples/javaconfig/hellowebflux/src/main/java/sample/HelloWebfluxApplication.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://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 sample; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.*; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import reactor.ipc.netty.NettyContext; -import reactor.ipc.netty.http.server.HttpServer; - -/** - * @author Rob Winch - * @since 5.0 - */ -@Configuration -@EnableWebFlux -@ComponentScan -public class HelloWebfluxApplication { - @Value("${server.port:8080}") - private int port = 8080; - - public static void main(String[] args) throws Exception { - try(AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HelloWebfluxApplication.class)) { - context.getBean(NettyContext.class).onClose().block(); - } - } - - @Profile("default") - @Bean - public NettyContext nettyContext(ApplicationContext context) { - HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context) - .build(); - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); - HttpServer httpServer = HttpServer.create("localhost", port); - return httpServer.newHandler(adapter).block(); - } -} diff --git a/samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle b/samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle deleted file mode 100644 index 33375a6fb71..00000000000 --- a/samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle +++ /dev/null @@ -1,16 +0,0 @@ -apply plugin: 'io.spring.convention.spring-sample' - -dependencies { - compile project(':spring-security-core') - compile project(':spring-security-config') - compile project(':spring-security-web') - compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'io.projectreactor.ipc:reactor-netty' - compile 'org.springframework:spring-context' - compile 'org.springframework:spring-webflux' - - testCompile project(':spring-security-test') - testCompile 'io.projectreactor:reactor-test' - testCompile 'org.skyscreamer:jsonassert' - testCompile 'org.springframework:spring-test' -} diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java deleted file mode 100644 index 4885ae60580..00000000000 --- a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://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 sample; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.*; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.reactive.function.server.HandlerStrategies; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.RouterFunctions; -import org.springframework.web.reactive.function.server.ServerResponse; -import org.springframework.web.server.WebFilter; -import reactor.ipc.netty.NettyContext; -import reactor.ipc.netty.http.server.HttpServer; - -import static org.springframework.web.reactive.function.server.RequestPredicates.GET; -import static org.springframework.web.reactive.function.server.RouterFunctions.route; - -/** - * @author Rob Winch - * @since 5.0 - */ -@Configuration -@EnableWebFlux -@ComponentScan -public class HelloWebfluxFnApplication { - @Value("${server.port:8080}") - private int port = 8080; - - public static void main(String[] args) throws Exception { - try(AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HelloWebfluxFnApplication.class)) { - context.getBean(NettyContext.class).onClose().block(); - } - } - - @Profile("default") - @Bean - public NettyContext nettyContext(HttpHandler handler) { - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); - HttpServer httpServer = HttpServer.create("localhost", this.port); - return httpServer.newHandler(adapter).block(); - } - - @Bean - public RouterFunction routes(HelloUserController userController) { - return route( - GET("/"), userController::hello); - } - - @Bean - public HttpHandler httpHandler(RouterFunction routes, WebFilter springSecurityFilterChain) { - HandlerStrategies handlerStrategies = HandlerStrategies.builder() - .webFilter(springSecurityFilterChain) - .build(); - - return RouterFunctions.toHttpHandler(routes, handlerStrategies); - } - -} diff --git a/samples/javaconfig/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java b/samples/javaconfig/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java index 0a118128d2a..6d72ee7c03f 100644 --- a/samples/javaconfig/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java +++ b/samples/javaconfig/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java @@ -37,7 +37,7 @@ public LoginPage(WebDriver webDriver) { } public LoginPage assertAt() { - assertThat(this.webDriver.getTitle()).isEqualTo("Login Page"); + assertThat(this.webDriver.getTitle()).isEqualTo("Please sign in"); return this; } @@ -49,7 +49,7 @@ public static class LoginForm { private WebDriver webDriver; private WebElement username; private WebElement password; - @FindBy(css = "input[type=submit]") + @FindBy(css = "button[type=submit]") private WebElement submit; public LoginForm(WebDriver webDriver) { diff --git a/samples/javaconfig/jdbc/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java b/samples/javaconfig/jdbc/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java index 0a118128d2a..6d72ee7c03f 100644 --- a/samples/javaconfig/jdbc/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java +++ b/samples/javaconfig/jdbc/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java @@ -37,7 +37,7 @@ public LoginPage(WebDriver webDriver) { } public LoginPage assertAt() { - assertThat(this.webDriver.getTitle()).isEqualTo("Login Page"); + assertThat(this.webDriver.getTitle()).isEqualTo("Please sign in"); return this; } @@ -49,7 +49,7 @@ public static class LoginForm { private WebDriver webDriver; private WebElement username; private WebElement password; - @FindBy(css = "input[type=submit]") + @FindBy(css = "button[type=submit]") private WebElement submit; public LoginForm(WebDriver webDriver) { diff --git a/samples/javaconfig/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java b/samples/javaconfig/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java index 0a118128d2a..6d72ee7c03f 100644 --- a/samples/javaconfig/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java +++ b/samples/javaconfig/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java @@ -37,7 +37,7 @@ public LoginPage(WebDriver webDriver) { } public LoginPage assertAt() { - assertThat(this.webDriver.getTitle()).isEqualTo("Login Page"); + assertThat(this.webDriver.getTitle()).isEqualTo("Please sign in"); return this; } @@ -49,7 +49,7 @@ public static class LoginForm { private WebDriver webDriver; private WebElement username; private WebElement password; - @FindBy(css = "input[type=submit]") + @FindBy(css = "button[type=submit]") private WebElement submit; public LoginForm(WebDriver webDriver) { diff --git a/samples/javaconfig/webflux-form/src/main/java/sample/ThymeleafConfig.java b/samples/javaconfig/webflux-form/src/main/java/sample/ThymeleafConfig.java deleted file mode 100644 index 8ea1e8e3b15..00000000000 --- a/samples/javaconfig/webflux-form/src/main/java/sample/ThymeleafConfig.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://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 sample; - -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.config.ViewResolverRegistry; -import org.springframework.web.reactive.config.WebFluxConfigurer; -import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine; -import org.thymeleaf.spring5.SpringWebFluxTemplateEngine; -import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; -import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver; -import org.thymeleaf.templatemode.TemplateMode; - -/** - * @author Rob Winch - * @since 5.0 - */ -@Configuration -public class ThymeleafConfig implements WebFluxConfigurer { - private ApplicationContext applicationContext; - - public ThymeleafConfig(final ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - - @Bean - public SpringResourceTemplateResolver thymeleafTemplateResolver() { - - SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver(); - resolver.setApplicationContext(this.applicationContext); - resolver.setPrefix("classpath:/templates/"); - resolver.setSuffix(".html"); - resolver.setTemplateMode(TemplateMode.HTML); - resolver.setCacheable(false); - resolver.setCheckExistence(true); - return resolver; - - } - - @Bean - public ISpringWebFluxTemplateEngine thymeleafTemplateEngine() { - SpringWebFluxTemplateEngine templateEngine = new SpringWebFluxTemplateEngine(); - templateEngine.setTemplateResolver(thymeleafTemplateResolver()); - return templateEngine; - } - - @Bean - public ThymeleafReactiveViewResolver thymeleafChunkedAndDataDrivenViewResolver() { - ThymeleafReactiveViewResolver viewResolver = new ThymeleafReactiveViewResolver(); - viewResolver.setTemplateEngine(thymeleafTemplateEngine()); - viewResolver.setOrder(1); - viewResolver.setResponseMaxChunkSizeBytes(8192); // OUTPUT BUFFER size limit - return viewResolver; - } - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - registry.viewResolver(thymeleafChunkedAndDataDrivenViewResolver()); - } -} diff --git a/samples/javaconfig/webflux-form/src/main/java/sample/WebfluxFormApplication.java b/samples/javaconfig/webflux-form/src/main/java/sample/WebfluxFormApplication.java deleted file mode 100644 index f942a1cf151..00000000000 --- a/samples/javaconfig/webflux-form/src/main/java/sample/WebfluxFormApplication.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://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 sample; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.*; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import reactor.ipc.netty.NettyContext; -import reactor.ipc.netty.http.server.HttpServer; - -/** - * @author Rob Winch - * @since 5.0 - */ -@Configuration -@EnableWebFlux -@ComponentScan -public class WebfluxFormApplication { - @Value("${server.port:8080}") - private int port = 8080; - - public static void main(String[] args) throws Exception { - try(AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - WebfluxFormApplication.class)) { - context.getBean(NettyContext.class).onClose().block(); - } - } - - @Profile("default") - @Bean - public NettyContext nettyContext(ApplicationContext context) { - HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context) - .build(); - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); - HttpServer httpServer = HttpServer.create("localhost", port); - return httpServer.newHandler(adapter).block(); - } -} diff --git a/samples/xml/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java b/samples/xml/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java index 0a118128d2a..6d72ee7c03f 100644 --- a/samples/xml/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java +++ b/samples/xml/helloworld/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java @@ -37,7 +37,7 @@ public LoginPage(WebDriver webDriver) { } public LoginPage assertAt() { - assertThat(this.webDriver.getTitle()).isEqualTo("Login Page"); + assertThat(this.webDriver.getTitle()).isEqualTo("Please sign in"); return this; } @@ -49,7 +49,7 @@ public static class LoginForm { private WebDriver webDriver; private WebElement username; private WebElement password; - @FindBy(css = "input[type=submit]") + @FindBy(css = "button[type=submit]") private WebElement submit; public LoginForm(WebDriver webDriver) { diff --git a/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java b/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java index 6a13e93eca8..1737af5de7e 100644 --- a/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java +++ b/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java @@ -39,7 +39,7 @@ public LoginPage(WebDriver webDriver) { } public LoginPage assertAt() { - assertThat(this.webDriver.getTitle()).isEqualTo("Login Page"); + assertThat(this.webDriver.getTitle()).isEqualTo("Please sign in"); return this; } @@ -51,7 +51,7 @@ public static class LoginForm { private WebDriver webDriver; private WebElement username; private WebElement password; - @FindBy(css = "input[type=submit]") + @FindBy(css = "button[type=submit]") private WebElement submit; public LoginForm(WebDriver webDriver) { diff --git a/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java b/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java index abaf6ccdb3f..a4a7e324eec 100644 --- a/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java +++ b/samples/xml/jaas/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java @@ -27,8 +27,8 @@ * @author Michael Simons */ public class LogoutPage extends LoginPage { - @FindBy(css = "p") - private WebElement p; + @FindBy(css = "div[role=alert]") + private WebElement alert; public LogoutPage(WebDriver webDriver) { super(webDriver); @@ -38,7 +38,7 @@ public LogoutPage(WebDriver webDriver) { public LogoutPage assertAt() { super.assertAt(); - assertThat(p.getText()).isEqualTo("You have been logged out"); + assertThat(this.alert.getText()).isEqualTo("You have been signed out"); return this; } } diff --git a/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java b/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java index 6a13e93eca8..1737af5de7e 100644 --- a/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java +++ b/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LoginPage.java @@ -39,7 +39,7 @@ public LoginPage(WebDriver webDriver) { } public LoginPage assertAt() { - assertThat(this.webDriver.getTitle()).isEqualTo("Login Page"); + assertThat(this.webDriver.getTitle()).isEqualTo("Please sign in"); return this; } @@ -51,7 +51,7 @@ public static class LoginForm { private WebDriver webDriver; private WebElement username; private WebElement password; - @FindBy(css = "input[type=submit]") + @FindBy(css = "button[type=submit]") private WebElement submit; public LoginForm(WebDriver webDriver) { diff --git a/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java b/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java index abaf6ccdb3f..a4a7e324eec 100644 --- a/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java +++ b/samples/xml/ldap/src/integration-test/java/org/springframework/security/samples/pages/LogoutPage.java @@ -27,8 +27,8 @@ * @author Michael Simons */ public class LogoutPage extends LoginPage { - @FindBy(css = "p") - private WebElement p; + @FindBy(css = "div[role=alert]") + private WebElement alert; public LogoutPage(WebDriver webDriver) { super(webDriver); @@ -38,7 +38,7 @@ public LogoutPage(WebDriver webDriver) { public LogoutPage assertAt() { super.assertAt(); - assertThat(p.getText()).isEqualTo("You have been logged out"); + assertThat(this.alert.getText()).isEqualTo("You have been signed out"); return this; } } diff --git a/scripts/s101.sh b/scripts/s101.sh new file mode 100755 index 00000000000..78f826d7e3a --- /dev/null +++ b/scripts/s101.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +./gradlew jar +mkdir -p build/s101 && cd $_ +rm *.jar +find ../../ -name '*-SNAPSHOT.jar' | grep -v samples | grep -v itest | xargs -I{} cp {} . diff --git a/settings.gradle b/settings.gradle index a2de87930e4..eeee3f19267 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,7 @@ rootProject.name = 'spring-security' FileTree buildFiles = fileTree(rootDir) { List excludes = gradle.startParameter.projectProperties.get("excludeProjects")?.split(",") include '**/*.gradle' - exclude '**/gradle', 'settings.gradle', 'buildSrc', '/build.gradle', '.*', 'out' + exclude 'build', '**/gradle', 'settings.gradle', 'buildSrc', '/build.gradle', '.*', 'out' exclude '**/grails3' if(excludes) { exclude excludes diff --git a/taglibs/src/test/java/org/springframework/security/taglibs/authz/AuthenticationTagTests.java b/taglibs/src/test/java/org/springframework/security/taglibs/authz/AuthenticationTagTests.java index 3c016df31a1..a07548783b2 100644 --- a/taglibs/src/test/java/org/springframework/security/taglibs/authz/AuthenticationTagTests.java +++ b/taglibs/src/test/java/org/springframework/security/taglibs/authz/AuthenticationTagTests.java @@ -101,7 +101,7 @@ public void testOperationWhenSecurityContextIsNull() throws Exception { authenticationTag.setProperty("principal"); assertThat(authenticationTag.doStartTag()).isEqualTo(Tag.SKIP_BODY); assertThat(authenticationTag.doEndTag()).isEqualTo(Tag.EVAL_PAGE); - assertThat(authenticationTag.getLastMessage()).isEqualTo(null); + assertThat(authenticationTag.getLastMessage()).isNull(); } @Test diff --git a/test/src/main/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListeners.java b/test/src/main/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListeners.java index d8cab45f09a..406841eaee7 100644 --- a/test/src/main/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListeners.java +++ b/test/src/main/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListeners.java @@ -36,6 +36,8 @@ * * @author Rob Winch * @since 4.0.2 + * @see WithSecurityContextTestExecutionListener + * @see ReactorContextTestExecutionListener */ @Documented @Inherited diff --git a/test/src/main/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListener.java b/test/src/main/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListener.java index c6616897aff..d6e0f10faa3 100644 --- a/test/src/main/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListener.java +++ b/test/src/main/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListener.java @@ -37,6 +37,8 @@ * * @author Rob Winch * @since 5.0 + * @see WithSecurityContextTestExecutionListener + * @see org.springframework.security.test.context.annotation.SecurityTestExecutionListeners */ public class ReactorContextTestExecutionListener extends DelegatingTestExecutionListener { diff --git a/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java b/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java index f9fd4ecd087..a26552e3d6b 100644 --- a/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java +++ b/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java @@ -43,6 +43,8 @@ * @author Rob Winch * @author EddĂș MelĂ©ndez * @since 4.0 + * @see ReactorContextTestExecutionListener + * @see org.springframework.security.test.context.annotation.SecurityTestExecutionListeners */ public class WithSecurityContextTestExecutionListener extends AbstractTestExecutionListener { diff --git a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java index 4e4951e74d8..2d2d73276a7 100644 --- a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java +++ b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java @@ -139,7 +139,7 @@ private CsrfMutator() {} } /** - * Updates the WebServerExchange using {@code {@link SecurityMockServerConfigurers#mockUser(UserDetails)}. Defaults to use a + * Updates the WebServerExchange using {@code {@link SecurityMockServerConfigurers#mockUser(UserDetails)}}. Defaults to use a * password of "password" and granted authorities of "ROLE_USER". */ public static class UserExchangeMutator implements WebTestClientConfigurer, MockServerConfigurer { diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java index 62e54519968..b2a924ccc9c 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java @@ -139,7 +139,7 @@ public MockHttpServletRequest buildRequest(ServletContext servletContext) { * Specifies the URL to POST to. Default is "/login" * * @param loginProcessingUrl the URL to POST to. Default is "/login" - * @return + * @return the {@link FormLoginRequestBuilder} for additional customizations */ public FormLoginRequestBuilder loginProcessingUrl(String loginProcessingUrl) { this.loginProcessingUrl = loginProcessingUrl; diff --git a/web/src/main/java/org/springframework/security/web/FilterChainProxy.java b/web/src/main/java/org/springframework/security/web/FilterChainProxy.java index 040a263bcdd..959f9b0d49e 100644 --- a/web/src/main/java/org/springframework/security/web/FilterChainProxy.java +++ b/web/src/main/java/org/springframework/security/web/FilterChainProxy.java @@ -238,7 +238,7 @@ private List getFilters(HttpServletRequest request) { * @return matching filter list */ public List getFilters(String url) { - return getFilters(firewall.getFirewalledRequest((new FilterInvocation(url, null) + return getFilters(firewall.getFirewalledRequest((new FilterInvocation(url, "GET") .getRequest()))); } diff --git a/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandler.java b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandler.java new file mode 100644 index 00000000000..22e8eb10cdf --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandler.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 java.util.LinkedHashMap; +import java.util.Map.Entry; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +/** + * An {@link AccessDeniedHandler} that delegates to other {@link AccessDeniedHandler} + * instances based upon the type of {@link HttpServletRequest} passed into + * {@link #handle(HttpServletRequest, HttpServletResponse, AccessDeniedException)}. + * + * @author Josh Cummings + * @since 5.1 + * + */ +public final class RequestMatcherDelegatingAccessDeniedHandler implements AccessDeniedHandler { + private final LinkedHashMap handlers; + + private final AccessDeniedHandler defaultHandler; + + /** + * Creates a new instance + * + * @param handlers a map of {@link RequestMatcher}s to + * {@link AccessDeniedHandler}s that should be used. Each is considered in the order + * they are specified and only the first {@link AccessDeniedHandler} is used. + * @param defaultHandler the default {@link AccessDeniedHandler} that should be used + * if none of the matchers match. + */ + public RequestMatcherDelegatingAccessDeniedHandler( + LinkedHashMap handlers, + AccessDeniedHandler defaultHandler) { + Assert.notEmpty(handlers, "handlers cannot be null or empty"); + Assert.notNull(defaultHandler, "defaultHandler cannot be null"); + this.handlers = new LinkedHashMap<>(handlers); + this.defaultHandler = defaultHandler; + } + + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, + ServletException { + for (Entry entry : this.handlers + .entrySet()) { + RequestMatcher matcher = entry.getKey(); + if (matcher.matches(request)) { + AccessDeniedHandler handler = entry.getValue(); + handler.handle(request, response, accessDeniedException); + return; + } + } + defaultHandler.handle(request, response, accessDeniedException); + } + +} 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 a0d683e5300..153b527c35a 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 @@ -22,6 +22,7 @@ import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; import org.springframework.util.Assert; import org.springframework.web.filter.GenericFilterBean; +import org.springframework.web.util.HtmlUtils; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -207,7 +208,7 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) { - String errorMsg = "none"; + String errorMsg = "Invalid credentials"; if (loginError) { HttpSession session = request.getSession(false); @@ -215,82 +216,76 @@ private String generateLoginPageHtml(HttpServletRequest request, boolean loginEr if (session != null) { AuthenticationException ex = (AuthenticationException) session .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); - errorMsg = ex != null ? ex.getMessage() : "none"; + errorMsg = ex != null ? ex.getMessage() : "Invalid credentials"; } } StringBuilder sb = new StringBuilder(); - sb.append("Login Page"); - - if (formLoginEnabled) { - sb.append("\n"); - } - - if (loginError) { - sb.append("

      Your login attempt was not successful, try again.

      Reason: "); - sb.append(errorMsg); - sb.append("

      "); - } - - if (logoutSuccess) { - sb.append("

      You have been logged out

      "); - } - - if (formLoginEnabled) { - sb.append("

      Login with Username and Password

      "); - sb.append("
      \n"); - sb.append("\n"); - sb.append(" \n"); - sb.append(" \n"); - - if (rememberMeParameter != null) { - sb.append(" \n"); - } - - sb.append(" \n"); - renderHiddenInputs(sb, request); - sb.append("
      User:
      Password:
      Remember me on this computer.
      \n"); - sb.append("
      "); + sb.append("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Please sign in\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
      \n"); + + String contextPath = request.getContextPath(); + if (this.formLoginEnabled) { + sb.append("
      \n" + + " \n" + + createError(loginError, errorMsg) + + createLogoutSuccess(logoutSuccess) + + "

      \n" + + " \n" + + " \n" + + "

      \n" + + "

      \n" + + " \n" + + " \n" + + "

      \n" + + createRememberMe(this.rememberMeParameter) + + renderHiddenInputs(request) + + " \n" + + "
      \n"); } if (openIdEnabled) { - sb.append("

      Login with OpenID Identity

      "); - sb.append("
      \n"); - sb.append("\n"); - sb.append(" \n"); - - if (openIDrememberMeParameter != null) { - sb.append(" \n"); - } - - sb.append(" \n"); - sb.append("
      Identity:
      Remember me on this computer.
      \n"); - renderHiddenInputs(sb, request); - sb.append("
      "); + sb.append("
      \n" + + " \n" + + createError(loginError, errorMsg) + + createLogoutSuccess(logoutSuccess) + + "

      \n" + + " \n" + + " \n" + + "

      \n" + + createRememberMe(this.openIDrememberMeParameter) + + renderHiddenInputs(request) + + " \n" + + "
      \n"); } if (oauth2LoginEnabled) { - sb.append("

      Login with OAuth 2.0

      "); - sb.append("\n"); + sb.append(""); + sb.append(createError(loginError, errorMsg)); + sb.append(createLogoutSuccess(logoutSuccess)); + sb.append("
      \n"); for (Map.Entry clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) { sb.append(" \n"); } - sb.append("
      "); - sb.append(""); - sb.append(clientAuthenticationUrlToClientName.getValue()); + String url = clientAuthenticationUrlToClientName.getKey(); + sb.append(""); + String clientName = HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue()); + sb.append(clientName); sb.append(""); sb.append("
      \n"); + sb.append("
      \n"); } sb.append(""); @@ -298,10 +293,21 @@ private String generateLoginPageHtml(HttpServletRequest request, boolean loginEr return sb.toString(); } - private void renderHiddenInputs(StringBuilder sb, HttpServletRequest request) { + private String renderHiddenInputs(HttpServletRequest request) { + StringBuilder sb = new StringBuilder(); for(Map.Entry input : this.resolveHiddenInputs.apply(request).entrySet()) { - sb.append(" \n"); + sb.append("\n"); + } + return sb.toString(); + } + + private String createRememberMe(String paramName) { + if (paramName == null) { + return ""; } + return "

      Remember me on this computer.

      \n"; } private boolean isLogoutSuccess(HttpServletRequest request) { @@ -316,6 +322,14 @@ private boolean isErrorPage(HttpServletRequest request) { return matches(request, failureUrl); } + private static String createError(boolean isError, String message) { + return isError ? "
      " + HtmlUtils.htmlEscape(message) + "
      " : ""; + } + + private static String createLogoutSuccess(boolean isLogoutSuccess) { + return isLogoutSuccess ? "
      You have been signed out
      " : ""; + } + private boolean matches(HttpServletRequest request, String url) { if (!"GET".equals(request.getMethod()) || url == null) { return false; 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 new file mode 100644 index 00000000000..70186605b07 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.ui; + +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; + +/** + * Generates a default log out page. + * + * @author Rob Winch + * @since 5.1 + */ +public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter { + private RequestMatcher matcher = new AntPathRequestMatcher("/logout", "GET"); + + private Function> resolveHiddenInputs = request -> Collections + .emptyMap(); + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (this.matcher.matches(request)) { + renderLogout(request, response); + } else { + filterChain.doFilter(request, response); + } + } + + private void renderLogout(HttpServletRequest request, HttpServletResponse response) + throws IOException { + String page = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Confirm Log Out?\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
      \n" + + "
      \n" + + " \n" + + renderHiddenInputs(request) + + " \n" + + "
      \n" + + "
      \n" + + " \n" + + ""; + + response.setContentType("text/html;charset=UTF-8"); + response.getWriter().write(page); + } + + /** + * Sets a Function used to resolve a Map of the hidden inputs where the key is the + * name of the input and the value is the value of the input. Typically this is used + * to resolve the CSRF token. + * @param resolveHiddenInputs the function to resolve the inputs + */ + public void setResolveHiddenInputs( + Function> resolveHiddenInputs) { + Assert.notNull(resolveHiddenInputs, "resolveHiddenInputs cannot be null"); + this.resolveHiddenInputs = resolveHiddenInputs; + } + + private String renderHiddenInputs(HttpServletRequest request) { + StringBuilder sb = new StringBuilder(); + for(Map.Entry input : this.resolveHiddenInputs.apply(request).entrySet()) { + sb.append("\n"); + } + return sb.toString(); + } +} 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 dbd4fa8f172..28f7354068a 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 @@ -154,7 +154,7 @@ protected void doFilterInternal(HttpServletRequest request, String header = request.getHeader("Authorization"); - if (header == null || !header.startsWith("Basic ")) { + if (header == null || !header.toLowerCase().startsWith("basic ")) { chain.doFilter(request, response); return; } diff --git a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java index 49f541f9d2b..01f84c3063b 100644 --- a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java @@ -25,9 +25,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.Authentication; +import org.springframework.security.core.Transient; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -387,6 +390,10 @@ private boolean contextChanged(SecurityContext context) { } private HttpSession createNewSessionIfAllowed(SecurityContext context) { + if (isTransientAuthentication(context.getAuthentication())) { + return null; + } + if (httpSessionExistedAtStartOfRequest) { if (logger.isDebugEnabled()) { logger.debug("HttpSession is now null, but was not null at start of request; " @@ -437,6 +444,10 @@ private HttpSession createNewSessionIfAllowed(SecurityContext context) { } } + private boolean isTransientAuthentication(Authentication authentication) { + return AnnotationUtils.getAnnotation(authentication.getClass(), Transient.class) != null; + } + /** * Sets the {@link AuthenticationTrustResolver} to be used. The default is * {@link AuthenticationTrustResolverImpl}. 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 ddfc2e8faa8..03eaeeec164 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 @@ -16,6 +16,8 @@ package org.springframework.security.web.firewall; +import org.springframework.http.HttpMethod; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Arrays; @@ -35,6 +37,11 @@ *

      *
        *
      • + * Rejects HTTP methods that are not allowed. This specified to block + * HTTP Verb tampering and XST attacks. + * See {@link #setAllowedHttpMethods(Collection)} + *
      • + *
      • * Rejects URLs that are not normalized to avoid bypassing security constraints. There is * no way to disable this as it is considered extremely risky to disable this constraint. * A few options to allow this behavior is to normalize the request prior to the firewall @@ -66,6 +73,11 @@ * @since 4.2.4 */ public class StrictHttpFirewall implements HttpFirewall { + /** + * Used to specify to {@link #setAllowedHttpMethods(Collection)} that any HTTP method should be allowed. + */ + private static final Set ALLOW_ANY_HTTP_METHOD = Collections.unmodifiableSet(Collections.emptySet()); + private static final String ENCODED_PERCENT = "%25"; private static final String PERCENT = "%"; @@ -82,6 +94,8 @@ public class StrictHttpFirewall implements HttpFirewall { private Set decodedUrlBlacklist = new HashSet(); + private Set allowedHttpMethods = createDefaultAllowedHttpMethods(); + public StrictHttpFirewall() { urlBlacklistsAddAll(FORBIDDEN_SEMICOLON); urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH); @@ -92,6 +106,39 @@ public StrictHttpFirewall() { this.decodedUrlBlacklist.add(PERCENT); } + /** + * Sets if any HTTP method is allowed. If this set to true, then no validation on the HTTP method will be performed. + * This can open the application up to + * HTTP Verb tampering and XST attacks + * @param unsafeAllowAnyHttpMethod if true, disables HTTP method validation, else resets back to the defaults. Default is false. + * @see #setAllowedHttpMethods(Collection) + * @since 5.1 + */ + public void setUnsafeAllowAnyHttpMethod(boolean unsafeAllowAnyHttpMethod) { + this.allowedHttpMethods = unsafeAllowAnyHttpMethod ? ALLOW_ANY_HTTP_METHOD : createDefaultAllowedHttpMethods(); + } + + /** + *

        + * Determines which HTTP methods should be allowed. The default is to allow "DELETE", "GET", "HEAD", "OPTIONS", + * "PATCH", "POST", and "PUT". + *

        + * + * @param allowedHttpMethods the case-sensitive collection of HTTP methods that are allowed. + * @see #setUnsafeAllowAnyHttpMethod(boolean) + * @since 5.1 + */ + public void setAllowedHttpMethods(Collection allowedHttpMethods) { + if (allowedHttpMethods == null) { + throw new IllegalArgumentException("allowedHttpMethods cannot be null"); + } + if (allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) { + this.allowedHttpMethods = ALLOW_ANY_HTTP_METHOD; + } else { + this.allowedHttpMethods = new HashSet<>(allowedHttpMethods); + } + } + /** *

        * Determines if semicolon is allowed in the URL (i.e. matrix variables). The default @@ -242,6 +289,7 @@ private void urlBlacklistsRemoveAll(Collection values) { @Override public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException { + rejectForbiddenHttpMethod(request); rejectedBlacklistedUrls(request); if (!isNormalized(request)) { @@ -259,6 +307,18 @@ public void reset() { }; } + private void rejectForbiddenHttpMethod(HttpServletRequest request) { + if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) { + return; + } + if (!this.allowedHttpMethods.contains(request.getMethod())) { + throw new RequestRejectedException("The request was rejected because the HTTP method \"" + + request.getMethod() + + "\" was not included within the whitelist " + + this.allowedHttpMethods); + } + } + private void rejectedBlacklistedUrls(HttpServletRequest request) { for (String forbidden : this.encodedUrlBlacklist) { if (encodedUrlContains(request, forbidden)) { @@ -277,6 +337,18 @@ public HttpServletResponse getFirewalledResponse(HttpServletResponse response) { return new FirewalledResponse(response); } + private static Set createDefaultAllowedHttpMethods() { + Set result = new HashSet<>(); + result.add(HttpMethod.DELETE.name()); + result.add(HttpMethod.GET.name()); + result.add(HttpMethod.HEAD.name()); + result.add(HttpMethod.OPTIONS.name()); + result.add(HttpMethod.PATCH.name()); + result.add(HttpMethod.POST.name()); + result.add(HttpMethod.PUT.name()); + return result; + } + private static boolean isNormalized(HttpServletRequest request) { if (!isNormalized(request.getRequestURI())) { return false; diff --git a/web/src/main/java/org/springframework/security/web/header/writers/CacheControlHeadersWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/CacheControlHeadersWriter.java index 9355ae6b987..9500e38a7ca 100644 --- a/web/src/main/java/org/springframework/security/web/header/writers/CacheControlHeadersWriter.java +++ b/web/src/main/java/org/springframework/security/web/header/writers/CacheControlHeadersWriter.java @@ -22,6 +22,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; import org.springframework.security.web.header.Header; import org.springframework.security.web.header.HeaderWriter; import org.springframework.util.ReflectionUtils; @@ -59,7 +60,7 @@ public CacheControlHeadersWriter() { @Override public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { if (hasHeader(response, CACHE_CONTROL) || hasHeader(response, EXPIRES) - || hasHeader(response, PRAGMA)) { + || hasHeader(response, PRAGMA) || response.getStatus() == HttpStatus.NOT_MODIFIED.value()) { return; } this.delegate.writeHeaders(request, response); diff --git a/web/src/main/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriter.java new file mode 100644 index 00000000000..1c5528b8979 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.header.writers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.web.header.HeaderWriter; +import org.springframework.util.Assert; + +/** + * Provides support for Feature + * Policy. + *

        + * Feature Policy allows web developers to selectively enable, disable, and modify the + * behavior of certain APIs and web features in the browser. + *

        + * A declaration of a feature policy contains a set of security policy directives, each + * responsible for declaring the restrictions for a particular feature type. + * + * @author Vedran Pavic + * @since 5.1 + */ +public final class FeaturePolicyHeaderWriter implements HeaderWriter { + + private static final String FEATURE_POLICY_HEADER = "Feature-Policy"; + + private String policyDirectives; + + /** + * Create a new instance of {@link FeaturePolicyHeaderWriter} with supplied security + * policy directive(s). + * + * @param policyDirectives the security policy directive(s) + * @throws IllegalArgumentException if policyDirectives is {@code null} or empty + */ + public FeaturePolicyHeaderWriter(String policyDirectives) { + setPolicyDirectives(policyDirectives); + } + + @Override + public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { + response.setHeader(FEATURE_POLICY_HEADER, this.policyDirectives); + } + + /** + * Set the security policy directive(s) to be used in the response header. + * + * @param policyDirectives the security policy directive(s) + * @throws IllegalArgumentException if policyDirectives is {@code null} or empty + */ + public void setPolicyDirectives(String policyDirectives) { + Assert.hasLength(policyDirectives, "policyDirectives must not be null or empty"); + this.policyDirectives = policyDirectives; + } + + @Override + public String toString() { + return getClass().getName() + " [policyDirectives=" + this.policyDirectives + "]"; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/http/SecurityHeaders.java b/web/src/main/java/org/springframework/security/web/http/SecurityHeaders.java new file mode 100644 index 00000000000..ef373724fcf --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/http/SecurityHeaders.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.http; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.Assert; + +import java.util.function.Consumer; + +/** + * Utilities for interacting with {@link HttpHeaders} + * + * @author Rob Winch + * @since 5.1 + */ +public final class SecurityHeaders { + + /** + * Sets the provided value as a Bearer token in a header with the name of {@link HttpHeaders#AUTHORIZATION} + * @param bearerTokenValue the bear token value + * @return a {@link Consumer} that sets the header. + */ + public static Consumer bearerToken(String bearerTokenValue) { + Assert.hasText(bearerTokenValue, "bearerTokenValue cannot be null"); + return headers -> headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + bearerTokenValue); + } + + private SecurityHeaders() {} +} diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/HttpSessionRequestCache.java b/web/src/main/java/org/springframework/security/web/savedrequest/HttpSessionRequestCache.java index a959afa9e2a..0cd238bdbfc 100644 --- a/web/src/main/java/org/springframework/security/web/savedrequest/HttpSessionRequestCache.java +++ b/web/src/main/java/org/springframework/security/web/savedrequest/HttpSessionRequestCache.java @@ -23,6 +23,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.security.web.PortResolver; import org.springframework.security.web.PortResolverImpl; +import org.springframework.security.web.util.UrlUtils; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -88,13 +89,9 @@ public void removeRequest(HttpServletRequest currentRequest, public HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response) { - DefaultSavedRequest saved = (DefaultSavedRequest) getRequest(request, response); + SavedRequest saved = getRequest(request, response); - if (saved == null) { - return null; - } - - if (!saved.doesRequestMatch(request, portResolver)) { + if (!matchesSavedRequest(request, saved)) { logger.debug("saved request doesn't match"); return null; } @@ -104,6 +101,20 @@ public HttpServletRequest getMatchingRequest(HttpServletRequest request, return new SavedRequestAwareWrapper(saved, request); } + private boolean matchesSavedRequest(HttpServletRequest request, SavedRequest savedRequest) { + if (savedRequest == null) { + return false; + } + + if (savedRequest instanceof DefaultSavedRequest) { + DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) savedRequest; + return defaultSavedRequest.doesRequestMatch(request, this.portResolver); + } + + String currentUrl = UrlUtils.buildFullRequestUrl(request); + return savedRequest.getRedirectUrl().equals(currentUrl); + } + /** * Allows selective use of saved requests for a subset of requests. By default any * request will be cached by the {@code saveRequest} method. 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 new file mode 100644 index 00000000000..c53839465a8 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/savedrequest/SimpleSavedRequest.java @@ -0,0 +1,131 @@ +/* + * 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 + * + * http://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.savedrequest; + +import org.springframework.util.Assert; + +import javax.servlet.http.Cookie; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * A Bean implementation of SavedRequest + * @author Rob Winch + * @since 5.1 + */ +public class SimpleSavedRequest implements SavedRequest { + private String redirectUrl; + + private List cookies = new ArrayList<>(); + + private String method = "GET"; + + private Map> headers = new HashMap<>(); + + private List locales = new ArrayList<>(); + + private Map parameters = new HashMap<>(); + + public SimpleSavedRequest() {} + + public SimpleSavedRequest(String redirectUrl) { + this.redirectUrl = redirectUrl; + } + + public SimpleSavedRequest(SavedRequest request) { + this.redirectUrl = request.getRedirectUrl(); + this.cookies = request.getCookies(); + for (String headerName : request.getHeaderNames()) { + this.headers.put(headerName, request.getHeaderValues(headerName)); + } + this.locales = request.getLocales(); + this.parameters = request.getParameterMap(); + } + + @Override + public String getRedirectUrl() { + return this.redirectUrl; + } + + @Override + public List getCookies() { + return this.cookies; + } + + @Override + public String getMethod() { + return null; + } + + @Override + public List getHeaderValues(String name) { + return this.headers.getOrDefault(name, new ArrayList<>()); + } + + @Override + public Collection getHeaderNames() { + return this.headers.keySet(); + } + + @Override + public List getLocales() { + return this.locales; + } + + @Override + public String[] getParameterValues(String name) { + return this.parameters.getOrDefault(name, new String[0]); + } + + @Override + public Map getParameterMap() { + return this.parameters; + } + + public void setRedirectUrl(String redirectUrl) { + Assert.notNull(redirectUrl, "redirectUrl cannot be null"); + this.redirectUrl = redirectUrl; + } + + public void setCookies(List cookies) { + Assert.notNull(cookies, "cookies cannot be null"); + this.cookies = cookies; + } + + public void setMethod(String method) { + Assert.notNull(method, "method cannot be null"); + this.method = method; + } + + public void setHeaders(Map> headers) { + Assert.notNull(headers, "headers cannot be null"); + this.headers = headers; + } + + public void setLocales(List locales) { + Assert.notNull("locales cannot be null"); + this.locales = locales; + } + + public void setParameters(Map parameters) { + Assert.notNull(parameters, "parameters cannot be null"); + this.parameters = parameters; + } +} 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 33ff40774d9..a1a52a4b45b 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 @@ -15,16 +15,15 @@ */ package org.springframework.security.web.server; -import java.util.function.Function; - -import org.springframework.util.Assert; -import reactor.core.publisher.Mono; - import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; +import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + /** * Converts a ServerWebExchange into a UsernamePasswordAuthenticationToken from the form * data HTTP parameters. @@ -32,14 +31,14 @@ * @author Rob Winch * @since 5.0 */ -public class ServerFormLoginAuthenticationConverter implements Function> { +public class ServerFormLoginAuthenticationConverter implements ServerAuthenticationConverter { private String usernameParameter = "username"; private String passwordParameter = "password"; @Override - public Mono apply(ServerWebExchange exchange) { + public Mono convert(ServerWebExchange exchange) { return exchange.getFormData() .map( data -> createAuthentication(data)); } diff --git a/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java index faaa19ed49d..245ecfd2081 100644 --- a/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java +++ b/web/src/main/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverter.java @@ -16,12 +16,12 @@ package org.springframework.security.web.server; import java.util.Base64; -import java.util.function.Function; import org.springframework.http.HttpHeaders; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @@ -32,16 +32,16 @@ * @author Rob Winch * @since 5.0 */ -public class ServerHttpBasicAuthenticationConverter implements Function> { +public class ServerHttpBasicAuthenticationConverter implements ServerAuthenticationConverter { public static final String BASIC = "Basic "; @Override - public Mono apply(ServerWebExchange exchange) { + public Mono convert(ServerWebExchange exchange) { ServerHttpRequest request = exchange.getRequest(); String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); - if(authorization == null) { + if(authorization == null || !authorization.toLowerCase().startsWith("basic ")) { return Mono.empty(); } diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java index bac03405799..d3a5d28ac93 100644 --- a/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java @@ -67,7 +67,7 @@ public class AuthenticationWebFilter implements WebFilter { private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new WebFilterChainServerAuthenticationSuccessHandler(); - private Function> authenticationConverter = new ServerHttpBasicAuthenticationConverter(); + private ServerAuthenticationConverter authenticationConverter = new ServerHttpBasicAuthenticationConverter(); private ServerAuthenticationFailureHandler authenticationFailureHandler = new ServerAuthenticationEntryPointFailureHandler(new HttpBasicServerAuthenticationEntryPoint()); @@ -88,7 +88,7 @@ public AuthenticationWebFilter(ReactiveAuthenticationManager authenticationManag public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { return this.requiresAuthenticationMatcher.matches(exchange) .filter( matchResult -> matchResult.isMatch()) - .flatMap( matchResult -> this.authenticationConverter.apply(exchange)) + .flatMap( matchResult -> this.authenticationConverter.convert(exchange)) .switchIfEmpty(chain.filter(exchange).then(Mono.empty())) .flatMap( token -> authenticate(exchange, chain, token)); } @@ -138,8 +138,23 @@ public void setAuthenticationSuccessHandler(ServerAuthenticationSuccessHandler a * that no authentication attempt should be made. The default converter is * {@link ServerHttpBasicAuthenticationConverter} * @param authenticationConverter the converter to use + * @deprecated As of 5.1 in favor of {@link #setAuthenticationConverter(ServerAuthenticationConverter)} + * @see #setAuthenticationConverter(ServerAuthenticationConverter) */ + @Deprecated public void setAuthenticationConverter(Function> authenticationConverter) { + setAuthenticationConverter((ServerAuthenticationConverter) authenticationConverter); + } + + /** + * Sets the strategy used for converting from a {@link ServerWebExchange} to an {@link Authentication} used for + * authenticating with the provided {@link ReactiveAuthenticationManager}. If the result is empty, then it signals + * that no authentication attempt should be made. The default converter is + * {@link ServerHttpBasicAuthenticationConverter} + * @param authenticationConverter the converter to use + * @since 5.1 + */ + public void setAuthenticationConverter(ServerAuthenticationConverter authenticationConverter) { Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); this.authenticationConverter = authenticationConverter; } @@ -156,7 +171,7 @@ public void setAuthenticationFailureHandler( /** * Sets the matcher used to determine when creating an {@link Authentication} from - * {@link #setAuthenticationConverter(Function)} to be authentication. If the converter returns an empty + * {@link #setAuthenticationConverter(ServerAuthenticationConverter)} to be authentication. If the converter returns an empty * result, then no authentication is attempted. The default is any request * @param requiresAuthenticationMatcher the matcher to use. Cannot be null. */ diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/ServerAuthenticationConverter.java b/web/src/main/java/org/springframework/security/web/server/authentication/ServerAuthenticationConverter.java new file mode 100644 index 00000000000..80146ebf360 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authentication/ServerAuthenticationConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.server.authentication; + +import org.springframework.security.core.Authentication; +import org.springframework.web.server.ServerWebExchange; + +import reactor.core.publisher.Mono; + +/** + * A strategy used for converting from a {@link ServerWebExchange} to an {@link Authentication} used for + * authenticating with a provided {@link org.springframework.security.authentication.ReactiveAuthenticationManager}. + * If the result is {@link Mono#empty()}, then it signals that no authentication attempt should be made. + * + * @author Eric Deandrea + * @since 5.1 + */ +@FunctionalInterface +public interface ServerAuthenticationConverter { + /** + * Converts a {@link ServerWebExchange} to an {@link Authentication} + * @param exchange The {@link ServerWebExchange} + * @return A {@link Mono} representing an {@link Authentication} + */ + Mono convert(ServerWebExchange exchange); +} diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java b/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java new file mode 100644 index 00000000000..f809f43f17e --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.server.authentication.logout; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.util.Assert; + +import reactor.core.publisher.Mono; + +/** + * Delegates to a collection of {@link ServerLogoutHandler} implementations. + * + * @author Eric Deandrea + * @since 5.1 + */ +public class DelegatingServerLogoutHandler implements ServerLogoutHandler { + private final List delegates; + + public DelegatingServerLogoutHandler(ServerLogoutHandler... delegates) { + Assert.notEmpty(delegates, "delegates cannot be null or empty"); + this.delegates = Arrays.asList(delegates); + } + + public DelegatingServerLogoutHandler(List delegates) { + Assert.notEmpty(delegates, "delegates cannot be null or empty"); + this.delegates = new ArrayList<>(delegates); + } + + @Override + public Mono logout(WebFilterExchange exchange, Authentication authentication) { + Stream> results = this.delegates.stream().map(delegate -> delegate.logout(exchange, authentication)); + return Mono.when(results.collect(Collectors.toList())); + } +} diff --git a/web/src/main/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriter.java index ac6b0949800..aae78795db0 100644 --- a/web/src/main/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriter.java +++ b/web/src/main/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriter.java @@ -16,6 +16,7 @@ package org.springframework.security.web.server.header; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @@ -61,6 +62,9 @@ public class CacheControlServerHttpHeadersWriter implements ServerHttpHeadersWri @Override public Mono writeHttpHeaders(ServerWebExchange exchange) { + if (exchange.getResponse().getStatusCode() == HttpStatus.NOT_MODIFIED) { + return Mono.empty(); + } return CACHE_HEADERS.writeHttpHeaders(exchange); } 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 373ee21e4b9..8e920af54ae 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 @@ -50,6 +50,12 @@ public class LoginPageGeneratingWebFilter implements WebFilter { private Map oauth2AuthenticationUrlToClientName = new HashMap<>(); + private boolean formLoginEnabled; + + public void setFormLoginEnabled(boolean enabled) { + this.formLoginEnabled = enabled; + } + public void setOauth2AuthenticationUrlToClientName( Map oauth2AuthenticationUrlToClientName) { Assert.notNull(oauth2AuthenticationUrlToClientName, "oauth2AuthenticationUrlToClientName cannot be null"); @@ -87,45 +93,48 @@ private Mono createBuffer(ServerWebExchange exchange) { private byte[] createPage(ServerWebExchange exchange, String csrfTokenHtmlInput) { MultiValueMap queryParams = exchange.getRequest() .getQueryParams(); - boolean isError = queryParams.containsKey("error"); - boolean isLogoutSuccess = queryParams.containsKey("logout"); String contextPath = exchange.getRequest().getPath().contextPath().value(); - String page = "\n" - + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Please sign in\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "

        \n" - + "
        \n" - + " \n" - + createError(isError) - + createLogoutSuccess(isLogoutSuccess) - + "

        \n" - + " \n" - + " \n" - + "

        \n" - + "

        \n" - + " \n" - + " \n" - + "

        \n" - + csrfTokenHtmlInput - + " \n" - + "
        \n" - + oauth2LoginLinks(contextPath, this.oauth2AuthenticationUrlToClientName) - + "
        \n" - + " \n" - + ""; + String page = "\n" + "\n" + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Please sign in\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
        \n" + + formLogin(queryParams, csrfTokenHtmlInput) + + oauth2LoginLinks(contextPath, this.oauth2AuthenticationUrlToClientName) + + "
        \n" + + " \n" + + ""; return page.getBytes(Charset.defaultCharset()); } + private String formLogin(MultiValueMap queryParams, String csrfTokenHtmlInput) { + if (!this.formLoginEnabled) { + return ""; + } + boolean isError = queryParams.containsKey("error"); + boolean isLogoutSuccess = queryParams.containsKey("logout"); + return "
        \n" + + " \n" + + createError(isError) + + createLogoutSuccess(isLogoutSuccess) + + "

        \n" + + " \n" + + " \n" + + "

        \n" + "

        \n" + + " \n" + + " \n" + + "

        \n" + csrfTokenHtmlInput + + " \n" + + "
        \n"; + } + private static String oauth2LoginLinks(String contextPath, Map oauth2AuthenticationUrlToClientName) { if (oauth2AuthenticationUrlToClientName.isEmpty()) { return ""; diff --git a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java index facc8208880..5205dfc34c4 100644 --- a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java @@ -123,6 +123,24 @@ protected final String getServletPath() { return this.servletPath; } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Mvc [pattern='").append(this.pattern).append("'"); + + if (this.servletPath != null) { + sb.append(", servletPath='").append(this.servletPath).append("'"); + } + + if (this.method != null) { + sb.append(", ").append(this.method); + } + + sb.append("]"); + + return sb.toString(); + } + private class DefaultMatcher implements RequestMatcher, RequestVariablesExtractor { private final UrlPathHelper pathHelper = new UrlPathHelper(); diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AnyRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AnyRequestMatcher.java index c18268d9f66..f3931d8ac03 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AnyRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AnyRequestMatcher.java @@ -44,6 +44,11 @@ public int hashCode() { return 1; } + @Override + public String toString() { + return "any request"; + } + private AnyRequestMatcher() { } } diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java index 501efe08b01..3a77019570c 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java @@ -65,4 +65,11 @@ public EvaluationContext createELContext(HttpServletRequest request) { return new StandardEvaluationContext(new ELRequestMatcherContext(request)); } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("EL [el=\"").append(this.expression.getExpressionString()).append("\""); + sb.append("]"); + return sb.toString(); + } } 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 64659649a90..0c54f8ad011 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 @@ -130,4 +130,18 @@ private static HttpMethod valueOf(String method) { return null; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Regex [pattern='").append(this.pattern).append("'"); + + if (this.httpMethod != null) { + sb.append(", ").append(this.httpMethod); + } + + sb.append("]"); + + return sb.toString(); + } } diff --git a/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java b/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java index 576fda3f75f..62738494247 100644 --- a/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java +++ b/web/src/test/java/org/springframework/security/web/FilterChainProxyTests.java @@ -69,7 +69,7 @@ public Object answer(InvocationOnMock inv) throws Throwable { fcp = new FilterChainProxy(new DefaultSecurityFilterChain(matcher, Arrays.asList(filter))); fcp.setFilterChainValidator(mock(FilterChainProxy.FilterChainValidator.class)); - request = new MockHttpServletRequest(); + request = new MockHttpServletRequest("GET", ""); request.setServletPath("/path"); response = new MockHttpServletResponse(); chain = mock(FilterChain.class); diff --git a/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandlerTests.java b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandlerTests.java new file mode 100644 index 00000000000..87e853ee733 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingAccessDeniedHandlerTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.util.LinkedHashMap; +import javax.servlet.http.HttpServletRequest; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Josh Cummings + */ +public class RequestMatcherDelegatingAccessDeniedHandlerTests { + private RequestMatcherDelegatingAccessDeniedHandler delegator; + private LinkedHashMap deniedHandlers; + private AccessDeniedHandler accessDeniedHandler; + private HttpServletRequest request; + + @Before + public void setup() { + this.accessDeniedHandler = mock(AccessDeniedHandler.class); + this.deniedHandlers = new LinkedHashMap<>(); + this.request = new MockHttpServletRequest(); + } + + @Test + public void handleWhenNothingMatchesThenOnlyDefaultHandlerInvoked() throws Exception { + AccessDeniedHandler handler = mock(AccessDeniedHandler.class); + RequestMatcher matcher = mock(RequestMatcher.class); + when(matcher.matches(this.request)).thenReturn(false); + this.deniedHandlers.put(matcher, handler); + this.delegator = new RequestMatcherDelegatingAccessDeniedHandler(this.deniedHandlers, this.accessDeniedHandler); + + this.delegator.handle(this.request, null, null); + + verify(this.accessDeniedHandler).handle(this.request, null, null); + verify(handler, never()).handle(this.request, null, null); + } + + @Test + public void handleWhenFirstMatchesThenOnlyFirstInvoked() throws Exception { + AccessDeniedHandler firstHandler = mock(AccessDeniedHandler.class); + RequestMatcher firstMatcher = mock(RequestMatcher.class); + AccessDeniedHandler secondHandler = mock(AccessDeniedHandler.class); + RequestMatcher secondMatcher = mock(RequestMatcher.class); + when(firstMatcher.matches(this.request)).thenReturn(true); + this.deniedHandlers.put(firstMatcher, firstHandler); + this.deniedHandlers.put(secondMatcher, secondHandler); + this.delegator = new RequestMatcherDelegatingAccessDeniedHandler(this.deniedHandlers, this.accessDeniedHandler); + + this.delegator.handle(this.request, null, null); + + verify(firstHandler).handle(this.request, null, null); + verify(secondHandler, never()).handle(this.request, null, null); + verify(this.accessDeniedHandler, never()).handle(this.request, null, null); + verify(secondMatcher, never()).matches(this.request); + } + + @Test + public void handleWhenSecondMatchesThenOnlySecondInvoked() throws Exception { + AccessDeniedHandler firstHandler = mock(AccessDeniedHandler.class); + RequestMatcher firstMatcher = mock(RequestMatcher.class); + AccessDeniedHandler secondHandler = mock(AccessDeniedHandler.class); + RequestMatcher secondMatcher = mock(RequestMatcher.class); + when(firstMatcher.matches(this.request)).thenReturn(false); + when(secondMatcher.matches(this.request)).thenReturn(true); + this.deniedHandlers.put(firstMatcher, firstHandler); + this.deniedHandlers.put(secondMatcher, secondHandler); + this.delegator = new RequestMatcherDelegatingAccessDeniedHandler(this.deniedHandlers, this.accessDeniedHandler); + + this.delegator.handle(this.request, null, null); + + verify(secondHandler).handle(this.request, null, null); + verify(firstHandler, never()).handle(this.request, null, null); + verify(this.accessDeniedHandler, never()).handle(this.request, null, null); + } +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java index 4f7b3ca9a41..ec7abb46640 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java @@ -15,6 +15,16 @@ */ package org.springframework.security.web.authentication; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.Collections; +import java.util.Locale; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.junit.Test; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.mock.web.MockHttpServletRequest; @@ -26,15 +36,6 @@ import org.springframework.security.web.WebAttributes; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; -import javax.servlet.FilterChain; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.Collections; -import java.util.Locale; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - /** * * @author Luke Taylor @@ -187,4 +188,21 @@ public void handlesNonIso8859CharsInErrorMessage() throws Exception { filter.doFilter(request, new MockHttpServletResponse(), chain); } + + // gh-5394 + @Test + public void generatesForOAuth2LoginAndEscapesClientName() throws Exception { + DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter(); + filter.setLoginPageUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL); + filter.setOauth2LoginEnabled(true); + + String clientName = "Google < > \" \' &"; + filter.setOauth2AuthenticationUrlToClientName( + Collections.singletonMap("/oauth2/authorization/google", clientName)); + + MockHttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(new MockHttpServletRequest("GET", "/login"), response, chain); + + assertThat(response.getContentAsString()).contains("Google < > " ' &"); + } } diff --git a/web/src/test/java/org/springframework/security/web/authentication/SimpleUrlAuthenticationSuccessHandlerTests.java b/web/src/test/java/org/springframework/security/web/authentication/SimpleUrlAuthenticationSuccessHandlerTests.java index cde2a256045..ae999320150 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/SimpleUrlAuthenticationSuccessHandlerTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/SimpleUrlAuthenticationSuccessHandlerTests.java @@ -108,7 +108,7 @@ public void setTargetUrlParameterNullTargetUrlParameter() { SimpleUrlAuthenticationSuccessHandler ash = new SimpleUrlAuthenticationSuccessHandler(); ash.setTargetUrlParameter("targetUrl"); ash.setTargetUrlParameter(null); - assertThat(ash.getTargetUrlParameter()).isEqualTo(null); + assertThat(ash.getTargetUrlParameter()).isNull(); } @Test diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java index 2ce52fa04c9..8989d388dae 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java @@ -147,7 +147,7 @@ public void nullPreAuthenticationClearsPreviousUser() throws Exception { filter.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(), new MockFilterChain()); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isEqualTo(null); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); } @Test 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 new file mode 100644 index 00000000000..bfb17149d6b --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilterTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.ui; + +import org.junit.Test; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Collections; + +import static org.hamcrest.core.StringContains.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; + +/** + * @author Rob Winch + * @since 5.1 + */ +public class DefaultLogoutPageGeneratingFilterTests { + private DefaultLogoutPageGeneratingFilter filter = new DefaultLogoutPageGeneratingFilter(); + + @Test + public void doFilterWhenNoHiddenInputsThenPageRendered() throws Exception { + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new Object()) + .addFilter(this.filter) + .build(); + + mockMvc.perform(get("/logout")) + .andExpect(content().string("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Confirm Log Out?\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
        \n" + + "
        \n" + + " \n" + + " \n" + + "
        \n" + + "
        \n" + + " \n" + + "")) + .andExpect(content().contentType("text/html;charset=UTF-8")); + } + + @Test + public void doFilterWhenHiddenInputsSetThenHiddenInputsRendered() throws Exception { + this.filter.setResolveHiddenInputs(r -> Collections.singletonMap("_csrf", "csrf-token-1")); + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new Object()) + .addFilters(this.filter) + .build(); + + mockMvc.perform(get("/logout")) + .andExpect(content().string(containsString(""))); + } + + @Test + public void doFilterWhenRequestContextThenActionContainsRequestContext() throws Exception { + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new Object()) + .addFilters(this.filter) + .build(); + + mockMvc.perform(get("/context/logout").contextPath("/context")) + .andExpect(content().string(containsString("action=\"/context/logout\""))); + } +} 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 d6beb9545c9..1c45cdfbfa0 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 @@ -156,6 +156,26 @@ public void testNormalOperation() throws Exception { .isEqualTo("rod"); } + // gh-5586 + @Test + public void doFilterWhenSchemeLowercaseThenCaseInsensitveMatchWorks() throws Exception { + String token = "rod:koala"; + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", + "basic " + new String(Base64.encodeBase64(token.getBytes()))); + request.setServletPath("/some_file.html"); + + // Test + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + FilterChain chain = mock(FilterChain.class); + filter.doFilter(request, new MockHttpServletResponse(), chain); + + verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); + assertThat(SecurityContextHolder.getContext().getAuthentication().getName()) + .isEqualTo("rod"); + } + @Test public void testOtherAuthorizationSchemeIsIgnored() throws Exception { diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryServlet25Tests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryServlet25Tests.java new file mode 100644 index 00000000000..5f9f4c28127 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryServlet25Tests.java @@ -0,0 +1,54 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://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.context; + +import javax.servlet.ServletRequest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.powermock.api.mockito.PowerMockito.spy; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * @author Luke Taylor + * @author Rob Winch + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ ClassUtils.class }) +public class HttpSessionSecurityContextRepositoryServlet25Tests { + @Test + public void servlet25Compatability() throws Exception { + spy(ClassUtils.class); + when(ClassUtils.class, "hasMethod", ServletRequest.class, "startAsync", + new Class[] {}).thenReturn(false); + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + repo.loadContext(holder); + assertThat(holder.getRequest()).isSameAs(request); + } +} diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java index 1b9c027fb01..56f4a78d315 100644 --- a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java @@ -16,18 +16,11 @@ package org.springframework.security.web.context; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.powermock.api.mockito.PowerMockito.mock; -import static org.powermock.api.mockito.PowerMockito.spy; -import static org.powermock.api.mockito.PowerMockito.when; -import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; - +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import javax.servlet.ServletOutputStream; -import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; @@ -36,25 +29,32 @@ import org.junit.After; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Transient; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.when; +import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; /** * @author Luke Taylor * @author Rob Winch */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({ ClassUtils.class }) public class HttpSessionSecurityContextRepositoryTests { private final TestingAuthenticationToken testToken = new TestingAuthenticationToken( @@ -65,20 +65,6 @@ public void tearDown() { SecurityContextHolder.clearContext(); } - @Test - public void servlet25Compatability() throws Exception { - spy(ClassUtils.class); - when(ClassUtils.class, "hasMethod", ServletRequest.class, "startAsync", - new Class[] {}).thenReturn(false); - HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, - response); - repo.loadContext(holder); - assertThat(holder.getRequest()).isSameAs(request); - } - @Test public void startAsyncDisablesSaveOnCommit() throws Exception { HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); @@ -633,4 +619,102 @@ public void failsWithStandardResponse() { repo.saveContext(context, request, response); } + + @Test + public void saveContextWhenTransientAuthenticationThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + SecurityContext context = repo.loadContext(holder); + + SomeTransientAuthentication authentication = new SomeTransientAuthentication(); + context.setAuthentication(authentication); + + repo.saveContext(context, holder.getRequest(), holder.getResponse()); + + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(session).isNull(); + } + + @Test + public void saveContextWhenTransientAuthenticationSubclassThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + SecurityContext context = repo.loadContext(holder); + + SomeTransientAuthenticationSubclass authentication = new SomeTransientAuthenticationSubclass(); + context.setAuthentication(authentication); + + repo.saveContext(context, holder.getRequest(), holder.getResponse()); + + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(session).isNull(); + } + + @Test + public void saveContextWhenTransientAuthenticationWithCustomAnnotationThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + SecurityContext context = repo.loadContext(holder); + + SomeOtherTransientAuthentication authentication = new SomeOtherTransientAuthentication(); + context.setAuthentication(authentication); + + repo.saveContext(context, holder.getRequest(), holder.getResponse()); + + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(session).isNull(); + } + + @Transient + private static class SomeTransientAuthentication extends AbstractAuthenticationToken { + public SomeTransientAuthentication() { + super(null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + } + + private static class SomeTransientAuthenticationSubclass extends SomeTransientAuthentication { + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Transient + public @interface TestTransientAuthentication { + } + + @TestTransientAuthentication + private static class SomeOtherTransientAuthentication extends AbstractAuthenticationToken { + public SomeOtherTransientAuthentication() { + super(null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + } } diff --git a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java index 5d2e57d3e78..27e2c0c3808 100644 --- a/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java +++ b/web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java @@ -16,11 +16,17 @@ package org.springframework.security.web.firewall; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; + +import java.util.Arrays; +import java.util.List; + import org.junit.Test; +import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; -import static org.assertj.core.api.Assertions.fail; - /** * @author Rob Winch */ @@ -31,12 +37,61 @@ public class StrictHttpFirewallTests { private StrictHttpFirewall firewall = new StrictHttpFirewall(); - private MockHttpServletRequest request = new MockHttpServletRequest(); + private MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); + + @Test + public void getFirewalledRequestWhenInvalidMethodThenThrowsRequestRejectedException() { + this.request.setMethod("INVALID"); + assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request)) + .isInstanceOf(RequestRejectedException.class); + } + + // blocks XST attacks + @Test + public void getFirewalledRequestWhenTraceMethodThenThrowsRequestRejectedException() { + this.request.setMethod(HttpMethod.TRACE.name()); + assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request)) + .isInstanceOf(RequestRejectedException.class); + } + + @Test + // blocks XST attack if request is forwarded to a Microsoft IIS web server + public void getFirewalledRequestWhenTrackMethodThenThrowsRequestRejectedException() { + this.request.setMethod("TRACK"); + assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request)) + .isInstanceOf(RequestRejectedException.class); + } + + @Test + // HTTP methods are case sensitive + public void getFirewalledRequestWhenLowercaseGetThenThrowsRequestRejectedException() { + this.request.setMethod("get"); + assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request)) + .isInstanceOf(RequestRejectedException.class); + } + + @Test + public void getFirewalledRequestWhenAllowedThenNoException() { + List allowedMethods = Arrays.asList("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"); + for (String allowedMethod : allowedMethods) { + this.request = new MockHttpServletRequest(allowedMethod, ""); + assertThatCode(() -> this.firewall.getFirewalledRequest(this.request)) + .doesNotThrowAnyException(); + } + } + + @Test + public void getFirewalledRequestWhenInvalidMethodAndAnyMethodThenNoException() { + this.firewall.setUnsafeAllowAnyHttpMethod(true); + this.request.setMethod("INVALID"); + assertThatCode(() -> this.firewall.getFirewalledRequest(this.request)) + .doesNotThrowAnyException(); + } @Test public void getFirewalledRequestWhenRequestURINotNormalizedThenThrowsRequestRejectedException() throws Exception { for (String path : this.unnormalizedPaths) { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setRequestURI(path); try { this.firewall.getFirewalledRequest(this.request); @@ -49,7 +104,7 @@ public void getFirewalledRequestWhenRequestURINotNormalizedThenThrowsRequestReje @Test public void getFirewalledRequestWhenContextPathNotNormalizedThenThrowsRequestRejectedException() throws Exception { for (String path : this.unnormalizedPaths) { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setContextPath(path); try { this.firewall.getFirewalledRequest(this.request); @@ -62,7 +117,7 @@ public void getFirewalledRequestWhenContextPathNotNormalizedThenThrowsRequestRej @Test public void getFirewalledRequestWhenServletPathNotNormalizedThenThrowsRequestRejectedException() throws Exception { for (String path : this.unnormalizedPaths) { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setServletPath(path); try { this.firewall.getFirewalledRequest(this.request); @@ -75,7 +130,7 @@ public void getFirewalledRequestWhenServletPathNotNormalizedThenThrowsRequestRej @Test public void getFirewalledRequestWhenPathInfoNotNormalizedThenThrowsRequestRejectedException() throws Exception { for (String path : this.unnormalizedPaths) { - this.request = new MockHttpServletRequest(); + this.request = new MockHttpServletRequest("GET", ""); this.request.setPathInfo(path); try { this.firewall.getFirewalledRequest(this.request); @@ -352,7 +407,7 @@ public void getFirewalledRequestWhenUppercaseEncodedPathThenException() { public void getFirewalledRequestWhenAllowUrlEncodedSlashAndLowercaseEncodedPathThenNoException() { this.firewall.setAllowUrlEncodedSlash(true); this.firewall.setAllowSemicolon(true); - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.setRequestURI("/context-root/a/b;%2f1/c"); request.setContextPath("/context-root"); request.setServletPath(""); @@ -365,7 +420,7 @@ public void getFirewalledRequestWhenAllowUrlEncodedSlashAndLowercaseEncodedPathT public void getFirewalledRequestWhenAllowUrlEncodedSlashAndUppercaseEncodedPathThenNoException() { this.firewall.setAllowUrlEncodedSlash(true); this.firewall.setAllowSemicolon(true); - MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); request.setRequestURI("/context-root/a/b;%2F1/c"); request.setContextPath("/context-root"); request.setServletPath(""); 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 a4e1b1bb42c..f774d3b3f08 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 @@ -23,6 +23,7 @@ import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.util.ReflectionUtils; @@ -124,4 +125,14 @@ public void writeHeadersDisabledIfExpires() { assertThat(this.response.getHeaderValue("Cache-Control")).isNull(); assertThat(this.response.getHeaderValue("Pragma")).isNull(); } + + @Test + // gh-5534 + public void writeHeadersDisabledIfNotModified() { + this.response.setStatus(HttpStatus.NOT_MODIFIED.value()); + + this.writer.writeHeaders(this.request, this.response); + + assertThat(this.response.getHeaderNames()).isEmpty(); + } } diff --git a/web/src/test/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriterTests.java new file mode 100644 index 00000000000..cf8b1fdeccd --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/header/writers/FeaturePolicyHeaderWriterTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.header.writers; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link FeaturePolicyHeaderWriter}. + * + * @author Vedran Pavic + */ +public class FeaturePolicyHeaderWriterTests { + + private static final String DEFAULT_POLICY_DIRECTIVES = "geolocation 'self'"; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private FeaturePolicyHeaderWriter writer; + + @Before + public void setUp() { + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + this.writer = new FeaturePolicyHeaderWriter(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void writeHeadersFeaturePolicyDefault() { + writer.writeHeaders(this.request, this.response); + + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeader("Feature-Policy")) + .isEqualTo(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void createWriterWithNullDirectivesShouldThrowException() { + assertThatThrownBy(() -> new FeaturePolicyHeaderWriter(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("policyDirectives must not be null or empty"); + } + + @Test + public void createWriterWithEmptyDirectivesShouldThrowException() { + assertThatThrownBy(() -> new FeaturePolicyHeaderWriter("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("policyDirectives must not be null or empty"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverterTests.java b/web/src/test/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverterTests.java index b10c1027eb3..294b532079d 100644 --- a/web/src/test/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/ServerFormLoginAuthenticationConverterTests.java @@ -55,7 +55,7 @@ public void applyWhenUsernameAndPasswordThenCreatesTokenSuccess() { this.data.add("username", username); this.data.add("password", password); - Authentication authentication = this.converter.apply(this.exchange).block(); + Authentication authentication = this.converter.convert(this.exchange).block(); assertThat(authentication.getName()).isEqualTo(username); assertThat(authentication.getCredentials()).isEqualTo(password); @@ -73,7 +73,7 @@ public void applyWhenCustomParametersAndUsernameAndPasswordThenCreatesTokenSucce this.data.add(usernameParameter, username); this.data.add(passwordParameter, password); - Authentication authentication = this.converter.apply(this.exchange).block(); + Authentication authentication = this.converter.convert(this.exchange).block(); assertThat(authentication.getName()).isEqualTo(username); assertThat(authentication.getCredentials()).isEqualTo(password); @@ -82,7 +82,7 @@ public void applyWhenCustomParametersAndUsernameAndPasswordThenCreatesTokenSucce @Test public void applyWhenNoDataThenCreatesTokenSuccess() { - Authentication authentication = this.converter.apply(this.exchange).block(); + Authentication authentication = this.converter.convert(this.exchange).block(); assertThat(authentication.getName()).isNullOrEmpty(); assertThat(authentication.getCredentials()).isNull(); diff --git a/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java b/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java index 22561ad5861..efc102a0dc4 100644 --- a/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/ServerHttpBasicAuthenticationConverterTests.java @@ -37,49 +37,65 @@ public class ServerHttpBasicAuthenticationConverterTests { @Test public void applyWhenNoAuthorizationHeaderThenEmpty() { - Mono result = apply(this.request); + Mono result = convert(this.request); assertThat(result.block()).isNull(); } @Test public void applyWhenEmptyAuthorizationHeaderThenEmpty() { - Mono result = apply(this.request.header(HttpHeaders.AUTHORIZATION, "")); + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "")); assertThat(result.block()).isNull(); } @Test public void applyWhenOnlyBasicAuthorizationHeaderThenEmpty() { - Mono result = apply(this.request.header(HttpHeaders.AUTHORIZATION, "Basic ")); + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "Basic ")); assertThat(result.block()).isNull(); } @Test public void applyWhenNotBase64ThenEmpty() { - Mono result = apply(this.request.header(HttpHeaders.AUTHORIZATION, "Basic z")); + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "Basic z")); assertThat(result.block()).isNull(); } @Test public void applyWhenNoSemicolonThenEmpty() { - Mono result = apply(this.request.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcg==")); + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcg==")); assertThat(result.block()).isNull(); } @Test public void applyWhenUserPasswordThenAuthentication() { - Mono result = apply(this.request.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==")); + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNzd29yZA==")); UsernamePasswordAuthenticationToken authentication = result.cast(UsernamePasswordAuthenticationToken.class).block(); assertThat(authentication.getPrincipal()).isEqualTo("user"); assertThat(authentication.getCredentials()).isEqualTo("password"); } - private Mono apply(MockServerHttpRequest.BaseBuilder request) { - return this.converter.apply(MockServerWebExchange.from(this.request.build())); + @Test + public void applyWhenLowercaseSchemeThenAuthentication() { + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "basic dXNlcjpwYXNzd29yZA==")); + + UsernamePasswordAuthenticationToken authentication = result.cast(UsernamePasswordAuthenticationToken.class).block(); + assertThat(authentication.getPrincipal()).isEqualTo("user"); + assertThat(authentication.getCredentials()).isEqualTo("password"); + } + + @Test + public void applyWhenWrongSchemeThenEmpty() { + Mono result = convert(this.request.header(HttpHeaders.AUTHORIZATION, "token dXNlcjpwYXNzd29yZA==")); + + assertThat(result.block()).isNull(); + } + + private Mono convert(MockServerHttpRequest.BaseBuilder request) { + return this.converter.convert(MockServerWebExchange.from(this.request.build())); } } diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java index b026040ef10..36839584d6b 100644 --- a/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java @@ -16,8 +16,6 @@ package org.springframework.security.web.server.authentication; -import java.util.function.Function; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,7 +32,6 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.test.web.reactive.server.EntityExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.server.ServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Matchers.any; @@ -56,7 +53,7 @@ public class AuthenticationWebFilterTests { @Mock private ServerAuthenticationSuccessHandler successHandler; @Mock - private Function> authenticationConverter; + private ServerAuthenticationConverter authenticationConverter; @Mock private ReactiveAuthenticationManager authenticationManager; @Mock @@ -140,7 +137,7 @@ public void filterWhenDefaultsAndAuthenticationFailThenUnauthorized() { @Test public void filterWhenConvertEmptyThenOk() { - when(this.authenticationConverter.apply(any())).thenReturn(Mono.empty()); + when(this.authenticationConverter.convert(any())).thenReturn(Mono.empty()); WebTestClient client = WebTestClientBuilder .bindToWebFilters(this.filter) @@ -161,7 +158,7 @@ public void filterWhenConvertEmptyThenOk() { @Test public void filterWhenConvertErrorThenServerError() { - when(this.authenticationConverter.apply(any())).thenReturn(Mono.error(new RuntimeException("Unexpected"))); + when(this.authenticationConverter.convert(any())).thenReturn(Mono.error(new RuntimeException("Unexpected"))); WebTestClient client = WebTestClientBuilder .bindToWebFilters(this.filter) @@ -182,7 +179,7 @@ public void filterWhenConvertErrorThenServerError() { @Test public void filterWhenConvertAndAuthenticationSuccessThenSuccess() { Mono authentication = Mono.just(new TestingAuthenticationToken("test", "this", "ROLE_USER")); - when(this.authenticationConverter.apply(any())).thenReturn(authentication); + when(this.authenticationConverter.convert(any())).thenReturn(authentication); when(this.authenticationManager.authenticate(any())).thenReturn(authentication); when(this.successHandler.onAuthenticationSuccess(any(), any())).thenReturn(Mono.empty()); when(this.securityContextRepository.save(any(), any())).thenAnswer( a -> Mono.just(a.getArguments()[0])); @@ -207,7 +204,7 @@ public void filterWhenConvertAndAuthenticationSuccessThenSuccess() { @Test public void filterWhenConvertAndAuthenticationEmptyThenServerError() { Mono authentication = Mono.just(new TestingAuthenticationToken("test", "this", "ROLE_USER")); - when(this.authenticationConverter.apply(any())).thenReturn(authentication); + when(this.authenticationConverter.convert(any())).thenReturn(authentication); when(this.authenticationManager.authenticate(any())).thenReturn(Mono.empty()); WebTestClient client = WebTestClientBuilder @@ -250,7 +247,7 @@ public void filterWhenNotMatchAndConvertAndAuthenticationSuccessThenContinues() @Test public void filterWhenConvertAndAuthenticationFailThenEntryPoint() { Mono authentication = Mono.just(new TestingAuthenticationToken("test", "this", "ROLE_USER")); - when(this.authenticationConverter.apply(any())).thenReturn(authentication); + when(this.authenticationConverter.convert(any())).thenReturn(authentication); when(this.authenticationManager.authenticate(any())).thenReturn(Mono.error(new BadCredentialsException("Failed"))); when(this.failureHandler.onAuthenticationFailure(any(), any())).thenReturn(Mono.empty()); @@ -273,7 +270,7 @@ public void filterWhenConvertAndAuthenticationFailThenEntryPoint() { @Test public void filterWhenConvertAndAuthenticationExceptionThenServerError() { Mono authentication = Mono.just(new TestingAuthenticationToken("test", "this", "ROLE_USER")); - when(this.authenticationConverter.apply(any())).thenReturn(authentication); + when(this.authenticationConverter.convert(any())).thenReturn(authentication); when(this.authenticationManager.authenticate(any())).thenReturn(Mono.error(new RuntimeException("Failed"))); WebTestClient client = WebTestClientBuilder diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java new file mode 100644 index 00000000000..4073c636ee7 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/authentication/logout/DelegatingServerLogoutHandlerTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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.server.authentication.logout; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.WebFilterExchange; + +import reactor.test.publisher.PublisherProbe; + +import java.util.List; + +/** + * @author Eric Deandrea + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class DelegatingServerLogoutHandlerTests { + @Mock + private ServerLogoutHandler delegate1; + + @Mock + private ServerLogoutHandler delegate2; + private PublisherProbe delegate1Result = PublisherProbe.empty(); + private PublisherProbe delegate2Result = PublisherProbe.empty(); + + @Mock + private WebFilterExchange exchange; + + @Mock + private Authentication authentication; + + @Before + public void setup() { + when(this.delegate1.logout(any(WebFilterExchange.class), any(Authentication.class))).thenReturn(this.delegate1Result.mono()); + when(this.delegate2.logout(any(WebFilterExchange.class), any(Authentication.class))).thenReturn(this.delegate2Result.mono()); + } + + @Test + public void constructorWhenNullVargsThenIllegalArgumentException() { + assertThatThrownBy(() -> new DelegatingServerLogoutHandler((ServerLogoutHandler[]) null)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("delegates cannot be null or empty") + .hasNoCause(); + } + + @Test + public void constructorWhenNullListThenIllegalArgumentException() { + assertThatThrownBy(() -> new DelegatingServerLogoutHandler((List) null)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("delegates cannot be null or empty") + .hasNoCause(); + } + + @Test + public void constructorWhenEmptyThenIllegalArgumentException() { + assertThatThrownBy(() -> new DelegatingServerLogoutHandler(new ServerLogoutHandler[0])) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("delegates cannot be null or empty") + .hasNoCause(); + } + + @Test + public void logoutWhenSingleThenExecuted() { + DelegatingServerLogoutHandler handler = new DelegatingServerLogoutHandler(this.delegate1); + handler.logout(this.exchange, this.authentication).block(); + + this.delegate1Result.assertWasSubscribed(); + } + + @Test + public void logoutWhenMultipleThenExecuted() { + DelegatingServerLogoutHandler handler = new DelegatingServerLogoutHandler(this.delegate1, this.delegate2); + handler.logout(this.exchange, this.authentication).block(); + + this.delegate1Result.assertWasSubscribed(); + this.delegate2Result.assertWasSubscribed(); + } +} diff --git a/web/src/test/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriterTests.java index 07248475da5..57f1d3c5727 100644 --- a/web/src/test/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/header/CacheControlServerHttpHeadersWriterTests.java @@ -19,6 +19,7 @@ import org.junit.Test; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.web.server.ServerWebExchange; @@ -83,4 +84,14 @@ public void writeHeadersWhenExpiresThenNoCacheControlHeaders() { assertThat(headers.get(HttpHeaders.EXPIRES)).containsOnly(expires); } + @Test + // gh-5534 + public void writeHeadersWhenNotModifiedThenNoCacheControlHeaders() { + exchange.getResponse().setStatusCode(HttpStatus.NOT_MODIFIED); + + writer.writeHttpHeaders(exchange); + + assertThat(headers).isEmpty(); + } + } diff --git a/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java index 7d3e260ac44..5cdd1346633 100644 --- a/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java @@ -216,4 +216,31 @@ public void matchesGetMatchableHandlerMappingThrows() throws Exception { new HttpRequestMethodNotSupportedException(this.request.getMethod())); assertThat(this.matcher.matches(this.request)).isTrue(); } + + @Test + public void toStringWhenAll() { + this.matcher.setMethod(HttpMethod.GET); + this.matcher.setServletPath("/spring"); + + assertThat(this.matcher.toString()).isEqualTo("Mvc [pattern='/path', servletPath='/spring', GET]"); + } + + @Test + public void toStringWhenHttpMethod() { + this.matcher.setMethod(HttpMethod.GET); + + assertThat(this.matcher.toString()).isEqualTo("Mvc [pattern='/path', GET]"); + } + + @Test + public void toStringWhenServletPath() { + this.matcher.setServletPath("/spring"); + + assertThat(this.matcher.toString()).isEqualTo("Mvc [pattern='/path', servletPath='/spring']"); + } + + @Test + public void toStringWhenOnlyPattern() { + assertThat(this.matcher.toString()).isEqualTo("Mvc [pattern='/path']"); + } } diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/ELRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/ELRequestMatcherTests.java index 66f70e2f0ad..b4b53dacee3 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/ELRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/ELRequestMatcherTests.java @@ -90,4 +90,10 @@ public void testHasHeaderNull() throws Exception { assertThat(requestMatcher.matches(request)).isFalse(); } + @Test + public void toStringThenFormatted() { + ELRequestMatcher requestMatcher = new ELRequestMatcher( + "hasHeader('User-Agent','MSIE')"); + assertThat(requestMatcher.toString()).isEqualTo("EL [el=\"hasHeader('User-Agent','MSIE')\"]"); + } } diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java index 78b1c2becd9..676be4310e8 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java @@ -108,6 +108,12 @@ public void matchesWithInvalidMethod() { assertThat(matcher.matches(request)).isFalse(); } + @Test + public void toStringThenFormatted() { + RegexRequestMatcher matcher = new RegexRequestMatcher("/blah", "GET"); + assertThat(matcher.toString()).isEqualTo("Regex [pattern='/blah', GET]"); + } + private HttpServletRequest createRequestWithNullMethod(String path) { when(request.getQueryString()).thenReturn("doesntMatter"); when(request.getServletPath()).thenReturn(path);