Compare commits
92 Commits
main
...
5.1.4.RELEASE
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c95c08106 | |||
| 091cbe5458 | |||
| 04f8a7974e | |||
| 66f30cf606 | |||
| e23e738cfe | |||
| 54e7aace6d | |||
| 4c1fdbf290 | |||
| 759c378eeb | |||
| 9f514753d5 | |||
| 16d5566cd6 | |||
| 7633fbfd8a | |||
| 51f905cb89 | |||
| 007ee7652b | |||
| 0aaaaf9f2f | |||
| be7d2a3c41 | |||
| e6aa141260 | |||
| e6bb9bfdca | |||
| 548dc4496c | |||
| 7f2f12c428 | |||
| 6dfad970d4 | |||
| 114e00f1dd | |||
| a0e541eb70 | |||
| 477a5a7cd3 | |||
| 8918a7e693 | |||
| bf33f9acef | |||
| f1598b985c | |||
| cc1ab2c368 | |||
| aac4525737 | |||
| eeb099a04f | |||
| 3e90c7663f | |||
| cfec514082 | |||
| 80b123bf36 | |||
| ee6f9e0b92 | |||
| 2625de9c23 | |||
| a145653030 | |||
| 8bc0ef86da | |||
| a02d6ad2a4 | |||
| acdd9ba1db | |||
| e218ac2e40 | |||
| dd859c4a6e | |||
| 5d85a1c9d8 | |||
| 49167458d2 | |||
| 6b4edacde2 | |||
| b7cebee89e | |||
| 01787ea168 | |||
| e3e9758da7 | |||
| 46012d62af | |||
| 6eb8129173 | |||
| 55dd732443 | |||
| 45cbdc03ba | |||
| d99c7dbfd1 | |||
| d8de4d06f7 | |||
| be2f0f0fae | |||
| 920d5ca6ad | |||
| 6dcf5a27b1 | |||
| c2166c4207 | |||
| 1542835268 | |||
| ae471e1456 | |||
| 853c54eecb | |||
| 10f6d10f81 | |||
| 02d844d528 | |||
| c0849ba891 | |||
| db145fa03f | |||
| 42b8b794a8 | |||
| 8014114225 | |||
| a742c0c3f2 | |||
| 1cec4fe3ac | |||
| 8655caa2de | |||
| dc5f5f348d | |||
| 35e05780ea | |||
| a461abcf32 | |||
| ba1f5f3873 | |||
| 5478b74116 | |||
| dc4aa3d017 | |||
| 2bc156ed19 | |||
| 0270994340 | |||
| a557a324c0 | |||
| 73022059d4 | |||
| 165cbcb723 | |||
| d99ecc2a7b | |||
| ccd2664e6c | |||
| b87ff3c00f | |||
| 8971777908 | |||
| 3d618d78ac | |||
| 697b8cd734 | |||
| cf03faf58e | |||
| 13159e9f88 | |||
| 41b7c74928 | |||
| 0a4ac4dbfc | |||
| 1eb56f46a5 | |||
| cfc3c25304 | |||
| 03a3a3b770 |
+1
-1
@@ -47,7 +47,7 @@ Create your topic branch to be submitted as a pull request from master. The Spri
|
||||
# Use short branch names
|
||||
Branches used when submitting pull requests should preferably be named according to GitHub issues, e.g. 'gh-1234' or 'gh-1234-fix-npe'. Otherwise, use succinct, lower-case, dash (-) delimited names, such as 'fix-warnings', 'fix-typo', etc. This is important, because branch names show up in the merge commits that result from accepting pull requests, and should be as expressive and concise as possible.
|
||||
|
||||
#Keep commits focused
|
||||
# Keep commits focused
|
||||
|
||||
Remember each ticket should be focused on a single item of interest since the tickets are used to produce the changelog. Since each commit should be tied to a single GitHub issue, ensure that your commits are focused. For example, do not include an update to a transitive library in your commit unless the GitHub is to update the library. Reviewing your commits is essential before sending a pull request.
|
||||
|
||||
|
||||
Vendored
+38
-1
@@ -30,7 +30,11 @@ try {
|
||||
checkout scm
|
||||
withCredentials([string(credentialsId: 'spring-sonar.login', variable: 'SONAR_LOGIN')]) {
|
||||
try {
|
||||
sh "./gradlew clean sonarqube -PexcludeProjects='**/samples/**' -Dsonar.host.url=$SPRING_SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN --refresh-dependencies --no-daemon --stacktrace"
|
||||
if ("master" == env.BRANCH_NAME) {
|
||||
sh "./gradlew sonarqube -PexcludeProjects='**/samples/**' -Dsonar.host.url=$SPRING_SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN --refresh-dependencies --no-daemon --stacktrace"
|
||||
} else {
|
||||
sh "./gradlew sonarqube -PexcludeProjects='**/samples/**' -Dsonar.projectKey='spring-security-${env.BRANCH_NAME}' -Dsonar.projectName='spring-security-${env.BRANCH_NAME}' -Dsonar.host.url=$SPRING_SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: sonar'
|
||||
throw e
|
||||
@@ -66,6 +70,36 @@ try {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
jdk10: {
|
||||
stage('JDK 10') {
|
||||
node {
|
||||
checkout scm
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk10' }"]) {
|
||||
sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: jdk10'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
jdk11: {
|
||||
stage('JDK 11') {
|
||||
node {
|
||||
checkout scm
|
||||
try {
|
||||
withEnv(["JAVA_HOME=${ tool 'jdk11' }"]) {
|
||||
sh "./gradlew clean test --refresh-dependencies --no-daemon --stacktrace"
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: jdk11'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(currentBuild.result == 'SUCCESS') {
|
||||
@@ -106,6 +140,9 @@ try {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
currentBuild.result = 'FAILED: deploys'
|
||||
throw e
|
||||
} finally {
|
||||
def buildStatus = currentBuild.result
|
||||
def buildNotSuccess = !SUCCESS.equals(buildStatus)
|
||||
|
||||
+2
-16
@@ -1,10 +1,11 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.18.RELEASE'
|
||||
classpath 'io.spring.gradle:spring-build-conventions:0.0.23.RELEASE'
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
|
||||
}
|
||||
repositories {
|
||||
maven { url 'https://repo.spring.io/plugins-snapshot' }
|
||||
maven { url 'https://plugins.gradle.org/m2/' }
|
||||
}
|
||||
}
|
||||
apply plugin: 'io.spring.convention.root'
|
||||
@@ -18,23 +19,8 @@ ext.milestoneBuild = !(snapshotBuild || releaseBuild)
|
||||
|
||||
dependencyManagementExport.projects = subprojects.findAll { !it.name.contains('-boot') }
|
||||
|
||||
// Disable JaCoCo when not explicitly requested to enable caching of test
|
||||
// See https://discuss.gradle.org/t/do-not-cache-if-condition-matched-jacoco-agent-configured-with-append-true-satisfied/23504
|
||||
gradle.taskGraph.whenReady { graph ->
|
||||
def enabled = graph.allTasks.any { it instanceof JacocoReport }
|
||||
subprojects { project ->
|
||||
project.plugins.withType(JacocoPlugin) {
|
||||
project.tasks.withType(Test) {
|
||||
jacoco.enabled = enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
subprojects {
|
||||
plugins.withType(JavaPlugin) {
|
||||
project.sourceCompatibility='1.8'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -200,6 +200,7 @@ public abstract class WebSecurityConfigurerAdapter implements
|
||||
|
||||
AuthenticationManager authenticationManager = authenticationManager();
|
||||
authenticationBuilder.parentAuthenticationManager(authenticationManager);
|
||||
authenticationBuilder.authenticationEventPublisher(eventPublisher);
|
||||
Map<Class<? extends Object>, Object> sharedObjects = createSharedObjects();
|
||||
|
||||
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
|
||||
|
||||
+7
@@ -25,6 +25,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
|
||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||
@@ -87,6 +88,12 @@ public final class RequestCacheConfigurer<H extends HttpSecurityBuilder<H>> exte
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public H disable() {
|
||||
getBuilder().setSharedObject(RequestCache.class, new NullRequestCache());
|
||||
return super.disable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(H http) throws Exception {
|
||||
http.setSharedObject(RequestCache.class, getRequestCache(http));
|
||||
|
||||
+1
-1
@@ -344,7 +344,7 @@ public final class UrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
* @return the {@link UrlAuthorizationConfigurer} for further customization
|
||||
*/
|
||||
public StandardInterceptUrlRegistry anonymous() {
|
||||
return hasRole("ROLE_ANONYMOUS");
|
||||
return hasRole("ANONYMOUS");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-3
@@ -183,9 +183,9 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
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().");
|
||||
"a jwk set uri by doing http.oauth2ResourceServer().jwt().jwkSetUri(uri), or wire a " +
|
||||
"JwtDecoder instance by doing http.oauth2ResourceServer().jwt().decoder(decoder), or " +
|
||||
"expose a JwtDecoder instance as a bean and do http.oauth2ResourceServer().jwt().");
|
||||
}
|
||||
|
||||
JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder();
|
||||
|
||||
+8
-5
@@ -45,6 +45,7 @@ import org.springframework.util.StringUtils;
|
||||
* Parser for the {@code CsrfFilter}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Ankur Pathak
|
||||
* @since 3.2
|
||||
*/
|
||||
public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
|
||||
@@ -67,11 +68,13 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
|
||||
boolean webmvcPresent = ClassUtils.isPresent(DISPATCHER_SERVLET_CLASS_NAME,
|
||||
getClass().getClassLoader());
|
||||
if (webmvcPresent) {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(
|
||||
CsrfRequestDataValueProcessor.class);
|
||||
BeanComponentDefinition componentDefinition = new BeanComponentDefinition(
|
||||
beanDefinition, REQUEST_DATA_VALUE_PROCESSOR);
|
||||
pc.registerBeanComponent(componentDefinition);
|
||||
if (!pc.getRegistry().containsBeanDefinition(REQUEST_DATA_VALUE_PROCESSOR)) {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(
|
||||
CsrfRequestDataValueProcessor.class);
|
||||
BeanComponentDefinition componentDefinition = new BeanComponentDefinition(
|
||||
beanDefinition, REQUEST_DATA_VALUE_PROCESSOR);
|
||||
pc.registerBeanComponent(componentDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
String matcherRef = null;
|
||||
|
||||
+1
-1
@@ -739,7 +739,7 @@ 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.
|
||||
## Specifies the maximum amount of time the host should be considered a Known HSTS Host. Default one year.
|
||||
attribute max-age-seconds {xsd:integer}?
|
||||
hsts-options.attlist &=
|
||||
## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true.
|
||||
|
||||
+1
-1
@@ -2299,7 +2299,7 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="max-age-seconds" type="xs:integer">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies the maximum ammount of time the host should be considered a Known HSTS Host.
|
||||
<xs:documentation>Specifies the maximum amount of time the host should be considered a Known HSTS Host.
|
||||
Default one year.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
+1
-1
@@ -759,7 +759,7 @@ 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.
|
||||
## Specifies the maximum amount of time the host should be considered a Known HSTS Host. Default one year.
|
||||
attribute max-age-seconds {xsd:integer}?
|
||||
hsts-options.attlist &=
|
||||
## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true.
|
||||
|
||||
+1
-1
@@ -2358,7 +2358,7 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="max-age-seconds" type="xs:integer">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies the maximum ammount of time the host should be considered a Known HSTS Host.
|
||||
<xs:documentation>Specifies the maximum amount of time the host should be considered a Known HSTS Host.
|
||||
Default one year.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
+1
-1
@@ -768,7 +768,7 @@ 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.
|
||||
## Specifies the maximum amount of time the host should be considered a Known HSTS Host. Default one year.
|
||||
attribute max-age-seconds {xsd:integer}?
|
||||
hsts-options.attlist &=
|
||||
## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true.
|
||||
|
||||
+1
-1
@@ -2387,7 +2387,7 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="max-age-seconds" type="xs:integer">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies the maximum ammount of time the host should be considered a Known HSTS Host.
|
||||
<xs:documentation>Specifies the maximum amount of time the host should be considered a Known HSTS Host.
|
||||
Default one year.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
+2
-2
@@ -748,7 +748,7 @@ 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
|
||||
## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository.
|
||||
attribute token-repository-ref { xsd:token }?
|
||||
|
||||
headers =
|
||||
@@ -770,7 +770,7 @@ 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.
|
||||
## Specifies the maximum amount of time the host should be considered a Known HSTS Host. Default one year.
|
||||
attribute max-age-seconds {xsd:integer}?
|
||||
hsts-options.attlist &=
|
||||
## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true.
|
||||
|
||||
+3
-2
@@ -2337,7 +2337,8 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="token-repository-ref" type="xs:token">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository
|
||||
<xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by
|
||||
LazyCsrfTokenRepository.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
@@ -2401,7 +2402,7 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="max-age-seconds" type="xs:integer">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies the maximum ammount of time the host should be considered a Known HSTS Host.
|
||||
<xs:documentation>Specifies the maximum amount of time the host should be considered a Known HSTS Host.
|
||||
Default one year.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
+2
-2
@@ -738,7 +738,7 @@ 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
|
||||
## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository.
|
||||
attribute token-repository-ref { xsd:token }?
|
||||
|
||||
headers =
|
||||
@@ -760,7 +760,7 @@ 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.
|
||||
## Specifies the maximum amount of time the host should be considered a Known HSTS Host. Default one year.
|
||||
attribute max-age-seconds {xsd:integer}?
|
||||
hsts-options.attlist &=
|
||||
## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true.
|
||||
|
||||
+3
-2
@@ -2232,7 +2232,8 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="token-repository-ref" type="xs:token">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository
|
||||
<xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by
|
||||
LazyCsrfTokenRepository.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
@@ -2296,7 +2297,7 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="max-age-seconds" type="xs:integer">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies the maximum ammount of time the host should be considered a Known HSTS Host.
|
||||
<xs:documentation>Specifies the maximum amount of time the host should be considered a Known HSTS Host.
|
||||
Default one year.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
+2
-2
@@ -738,7 +738,7 @@ 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
|
||||
## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository.
|
||||
attribute token-repository-ref { xsd:token }?
|
||||
|
||||
headers =
|
||||
@@ -760,7 +760,7 @@ 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.
|
||||
## Specifies the maximum amount of time the host should be considered a Known HSTS Host. Default one year.
|
||||
attribute max-age-seconds {xsd:integer}?
|
||||
hsts-options.attlist &=
|
||||
## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true.
|
||||
|
||||
+3
-2
@@ -2232,7 +2232,8 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="token-repository-ref" type="xs:token">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository
|
||||
<xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by
|
||||
LazyCsrfTokenRepository.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
@@ -2297,7 +2298,7 @@
|
||||
</xs:attribute>
|
||||
<xs:attribute name="max-age-seconds" type="xs:integer">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Specifies the maximum ammount of time the host should be considered a Known HSTS Host.
|
||||
<xs:documentation>Specifies the maximum amount of time the host should be considered a Known HSTS Host.
|
||||
Default one year.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
|
||||
+6
@@ -79,6 +79,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
|
||||
<input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</body></html>"""
|
||||
when: "fail to log in"
|
||||
super.setup()
|
||||
@@ -121,6 +122,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
|
||||
<input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</body></html>"""
|
||||
when: "login success"
|
||||
super.setup()
|
||||
@@ -168,6 +170,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
|
||||
<input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</body></html>"""
|
||||
}
|
||||
|
||||
@@ -272,6 +275,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
|
||||
<input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</body></html>"""
|
||||
}
|
||||
|
||||
@@ -318,6 +322,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
|
||||
<input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</body></html>"""
|
||||
}
|
||||
|
||||
@@ -377,6 +382,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
|
||||
<input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</body></html>"""
|
||||
}
|
||||
|
||||
|
||||
-209
@@ -1,209 +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.annotation.web.configurers
|
||||
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser
|
||||
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.config.annotation.AnyObjectPostProcessor
|
||||
import org.springframework.security.config.annotation.BaseSpringSpec
|
||||
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.web.savedrequest.RequestCache
|
||||
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
|
||||
|
||||
import spock.lang.Unroll;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
class RequestCacheConfigurerTests extends BaseSpringSpec {
|
||||
|
||||
def "requestCache ObjectPostProcessor"() {
|
||||
setup:
|
||||
AnyObjectPostProcessor opp = Mock()
|
||||
HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:])
|
||||
when:
|
||||
http
|
||||
.requestCache()
|
||||
.and()
|
||||
.build()
|
||||
|
||||
then: "RequestCacheAwareFilter is registered with LifecycleManager"
|
||||
1 * opp.postProcess(_ as RequestCacheAwareFilter) >> {RequestCacheAwareFilter o -> o}
|
||||
}
|
||||
|
||||
def "invoke requestCache twice does not reset"() {
|
||||
setup:
|
||||
RequestCache RC = Mock()
|
||||
AnyObjectPostProcessor opp = Mock()
|
||||
HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:])
|
||||
when:
|
||||
http
|
||||
.requestCache()
|
||||
.requestCache(RC)
|
||||
.and()
|
||||
.requestCache()
|
||||
|
||||
then:
|
||||
http.getSharedObject(RequestCache) == RC
|
||||
}
|
||||
|
||||
def "RequestCache disables faviocon.ico"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.servletPath = "/favicon.ico"
|
||||
request.requestURI = "/favicon.ico"
|
||||
request.method = "GET"
|
||||
when: "request favicon.ico"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to the login page"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/login"
|
||||
when: "authenticate successfully"
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to default URL since it was favicon.ico"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "/"
|
||||
}
|
||||
|
||||
def "RequestCache disables faviocon.png"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.servletPath = "/favicon.png"
|
||||
request.requestURI = "/favicon.png"
|
||||
request.method = "GET"
|
||||
when: "request favicon.ico"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to the login page"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/login"
|
||||
when: "authenticate successfully"
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to default URL since it was favicon.ico"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "/"
|
||||
}
|
||||
|
||||
def "SEC-2321: RequestCache disables application/json"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE)
|
||||
request.method = "GET"
|
||||
request.servletPath = "/messages"
|
||||
request.requestURI = "/messages"
|
||||
when: "request application/json"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to the login page"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/login"
|
||||
when: "authenticate successfully"
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to default URL since it was application/json. This is desirable since JSON requests are typically not invoked directly from the browser and we don't want the browser to replay them"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "/"
|
||||
}
|
||||
|
||||
def "SEC-2321: RequestCache disables X-Requested-With"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||
request.method = "GET"
|
||||
request.servletPath = "/messages"
|
||||
request.requestURI = "/messages"
|
||||
when: "request X-Requested-With"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to the login page"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/login"
|
||||
when: "authenticate successfully"
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to default URL since it was X-Requested-With"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "/"
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def "RequestCache saves #headerName: #headerValue"() {
|
||||
setup:
|
||||
loadConfig(RequestCacheDefautlsConfig)
|
||||
request.addHeader(headerName, headerValue)
|
||||
request.method = "GET"
|
||||
request.servletPath = "/messages"
|
||||
request.requestURI = "/messages"
|
||||
when: "request content type"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
super.setupWeb(request.session)
|
||||
request.servletPath = "/login"
|
||||
request.setParameter("username","user")
|
||||
request.setParameter("password","password")
|
||||
request.method = "POST"
|
||||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to saved URL"
|
||||
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
|
||||
response.redirectedUrl == "http://localhost/messages"
|
||||
where:
|
||||
headerName << ["Accept", "Accept", "Accept", "X-Requested-With"]
|
||||
headerValue << [MediaType.ALL_VALUE, MediaType.TEXT_HTML, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8","com.android"]
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class RequestCacheDefautlsConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||
auth
|
||||
.inMemoryAuthentication()
|
||||
.withUser(PasswordEncodedUser.user());
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-4
@@ -15,23 +15,31 @@
|
||||
*/
|
||||
package org.springframework.security.config;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Matchers.*;
|
||||
import static org.powermock.api.mockito.PowerMockito.*;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
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.beans.factory.parsing.BeanDefinitionParsingException;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.security.config.util.InMemoryXmlApplicationContext;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.powermock.api.mockito.PowerMockito.doThrow;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.spy;
|
||||
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.verifyZeroInteractions;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Luke Taylor
|
||||
@@ -40,6 +48,7 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({ ClassUtils.class })
|
||||
@PowerMockIgnore({ "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*" })
|
||||
public class SecurityNamespaceHandlerTests {
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
+7
-3
@@ -15,16 +15,15 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.powermock.api.mockito.PowerMockito.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.After;
|
||||
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.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
@@ -33,6 +32,10 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
|
||||
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
|
||||
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 Rob Winch
|
||||
@@ -40,6 +43,7 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationCon
|
||||
*/
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({ SpringFactoriesLoader.class })
|
||||
@PowerMockIgnore({ "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*" })
|
||||
public class WebSecurityConfigurerAdapterPowermockTests {
|
||||
ConfigurableWebApplicationContext context;
|
||||
|
||||
|
||||
+11
-8
@@ -15,12 +15,22 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
@@ -48,14 +58,6 @@ import org.springframework.web.context.request.async.WebAsyncManager;
|
||||
import org.springframework.web.context.request.async.WebAsyncUtils;
|
||||
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.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.ThrowableAssert.catchThrowable;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -75,6 +77,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
*/
|
||||
@PrepareForTest({WebAsyncManager.class})
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PowerMockIgnore({ "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*", "javax.xml.transform.*" })
|
||||
public class WebSecurityConfigurerAdapterTests {
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* 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 javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
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.core.userdetails.User;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.RequestBuilder;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Tests for {@link RequestCacheConfigurer}
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Josh Cummings
|
||||
*/
|
||||
public class RequestCacheConfigurerTests {
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
@Autowired
|
||||
MockMvc mvc;
|
||||
|
||||
@Test
|
||||
public void configureWhenRegisteringObjectPostProcessorThenInvokedOnExceptionTranslationFilter() {
|
||||
this.spring.register(ObjectPostProcessorConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
verify(ObjectPostProcessorConfig.objectPostProcessor)
|
||||
.postProcess(any(RequestCacheAwareFilter.class));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter {
|
||||
static ObjectPostProcessor<Object> objectPostProcessor = spy(ReflectingObjectPostProcessor.class);
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.requestCache();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
static ObjectPostProcessor<Object> objectPostProcessor() {
|
||||
return objectPostProcessor;
|
||||
}
|
||||
}
|
||||
|
||||
static class ReflectingObjectPostProcessor implements ObjectPostProcessor<Object> {
|
||||
@Override
|
||||
public <O> O postProcess(O object) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenInvokingExceptionHandlingTwiceThenOriginalEntryPointUsed() throws Exception {
|
||||
this.spring.register(InvokeTwiceDoesNotOverrideConfig.class).autowire();
|
||||
|
||||
this.mvc.perform(get("/"));
|
||||
|
||||
verify(InvokeTwiceDoesNotOverrideConfig.requestCache)
|
||||
.getMatchingRequest(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class InvokeTwiceDoesNotOverrideConfig extends WebSecurityConfigurerAdapter {
|
||||
static RequestCache requestCache = mock(RequestCache.class);
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.requestCache()
|
||||
.requestCache(requestCache)
|
||||
.and()
|
||||
.requestCache();
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedUrlIsFaviconIcoThenPostAuthenticationRedirectsToRoot() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/favicon.ico"))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("/")); // ignores favicon.ico
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedUrlIsFaviconPngThenPostAuthenticationRedirectsToRoot() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/favicon.png"))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("/")); // ignores favicon.png
|
||||
}
|
||||
|
||||
// SEC-2321
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsApplicationJsonThenPostAuthenticationRedirectsToRoot() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("/")); // ignores application/json
|
||||
|
||||
// This is desirable since JSON requests are typically not invoked directly from the browser and we don't want the browser to replay them
|
||||
}
|
||||
|
||||
// SEC-2321
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsXRequestedWithThenPostAuthenticationRedirectsToRoot() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header("X-Requested-With", "XMLHttpRequest"))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("/"));
|
||||
|
||||
// This is desirable since XHR requests are typically not invoked directly from the browser and we don't want the browser to replay them
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsAllMediaTypeThenPostAuthenticationRemembers() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header(HttpHeaders.ACCEPT, MediaType.ALL))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("http://localhost/messages"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsTextHtmlThenPostAuthenticationRemembers() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("http://localhost/messages"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsChromeThenPostAuthenticationRemembers() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header(HttpHeaders.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("http://localhost/messages"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenBookmarkedRequestIsRequestedWithAndroidThenPostAuthenticationRemembers() throws Exception {
|
||||
this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/messages")
|
||||
.header("X-Requested-With", "com.android"))
|
||||
.andExpect(redirectedUrl("http://localhost/login"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("http://localhost/messages"));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class RequestCacheDefaultsConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin();
|
||||
}
|
||||
}
|
||||
|
||||
// gh-6102
|
||||
@Test
|
||||
public void getWhenRequestCacheIsDisabledThenExceptionTranslationFilterDoesNotStoreRequest() throws Exception {
|
||||
this.spring.register(RequestCacheDisabledConfig.class, DefaultSecurityConfig.class).autowire();
|
||||
|
||||
MockHttpSession session = (MockHttpSession)
|
||||
this.mvc.perform(get("/bob"))
|
||||
.andReturn().getRequest().getSession();
|
||||
|
||||
this.mvc.perform(formLogin(session))
|
||||
.andExpect(redirectedUrl("/"));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class RequestCacheDisabledConfig extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
super.configure(http);
|
||||
http.requestCache().disable();
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class DefaultSecurityConfig {
|
||||
|
||||
@Bean
|
||||
public InMemoryUserDetailsManager userDetailsManager() {
|
||||
return new InMemoryUserDetailsManager(User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static RequestBuilder formLogin(MockHttpSession session) {
|
||||
return post("/login")
|
||||
.param("username", "user")
|
||||
.param("password", "password")
|
||||
.session(session)
|
||||
.with(csrf());
|
||||
}
|
||||
}
|
||||
+10
-8
@@ -15,15 +15,7 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.same;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.spy;
|
||||
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -32,8 +24,10 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
@@ -52,12 +46,20 @@ import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.same;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.spy;
|
||||
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({ ReflectionUtils.class, Method.class })
|
||||
@PowerMockIgnore({ "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*" })
|
||||
public class SessionManagementConfigurerServlet31Tests {
|
||||
@Mock
|
||||
Method method;
|
||||
|
||||
+19
@@ -41,6 +41,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
* @author M.S. Dousti
|
||||
*
|
||||
*/
|
||||
public class UrlAuthorizationConfigurerTests {
|
||||
@@ -203,6 +204,24 @@ public class UrlAuthorizationConfigurerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anonymousUrlAuthorization() {
|
||||
loadConfig(AnonymousUrlAuthorizationConfig.class);
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
static class AnonymousUrlAuthorizationConfig extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
public void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.apply(new UrlAuthorizationConfigurer<>(null)).getRegistry()
|
||||
.anyRequest().anonymous();
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
public void loadConfig(Class<?>... configs) {
|
||||
this.context = new AnnotationConfigWebApplicationContext();
|
||||
this.context.register(configs);
|
||||
|
||||
+8
-2
@@ -135,7 +135,10 @@ public class OAuth2ClientConfigurerTests {
|
||||
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");
|
||||
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");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -184,7 +187,10 @@ public class OAuth2ClientConfigurerTests {
|
||||
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%3A%2F%2Flocalhost%2Fclient-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://localhost/client-1");
|
||||
|
||||
verify(requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
+34
-1
@@ -21,6 +21,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.PropertyAccessorFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -28,6 +29,7 @@ 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.authentication.event.AuthenticationSuccessEvent;
|
||||
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;
|
||||
@@ -156,6 +158,30 @@ public class OAuth2LoginConfigurerTests {
|
||||
.isInstanceOf(OAuth2UserAuthority.class).hasToString("ROLE_USER");
|
||||
}
|
||||
|
||||
// gh-6009
|
||||
@Test
|
||||
public void oauth2LoginWhenSuccessThenAuthenticationSuccessEventPublished() throws Exception {
|
||||
// setup application context
|
||||
loadConfig(OAuth2LoginConfig.class);
|
||||
|
||||
// setup authorization request
|
||||
OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest();
|
||||
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
|
||||
assertThat(OAuth2LoginConfig.EVENTS).isNotEmpty();
|
||||
assertThat(OAuth2LoginConfig.EVENTS).hasSize(1);
|
||||
assertThat(OAuth2LoginConfig.EVENTS.get(0)).isInstanceOf(AuthenticationSuccessEvent.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oauth2LoginWhenAuthenticatedThenIgnored() throws Exception {
|
||||
// setup application context
|
||||
@@ -467,7 +493,9 @@ public class OAuth2LoginConfigurerTests {
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class OAuth2LoginConfig extends CommonWebSecurityConfigurerAdapter {
|
||||
static class OAuth2LoginConfig extends CommonWebSecurityConfigurerAdapter implements ApplicationListener<AuthenticationSuccessEvent> {
|
||||
static List<AuthenticationSuccessEvent> EVENTS = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
@@ -476,6 +504,11 @@ public class OAuth2LoginConfigurerTests {
|
||||
new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION));
|
||||
super.configure(http);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(AuthenticationSuccessEvent event) {
|
||||
EVENTS.add(event);
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.Test;
|
||||
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
|
||||
/**
|
||||
* @author Ankur Pathak
|
||||
*/
|
||||
public class CsrfBeanDefinitionParserTests {
|
||||
private static final String CONFIG_LOCATION_PREFIX =
|
||||
"classpath:org/springframework/security/config/http/CsrfBeanDefinitionParserTests";
|
||||
|
||||
@Test
|
||||
public void registerDataValueProcessorOnlyIfNotRegistered() throws Exception {
|
||||
try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext()) {
|
||||
context.setAllowBeanDefinitionOverriding(false);
|
||||
context.setConfigLocation(this.xml("RegisterDataValueProcessorOnyIfNotRegistered"));
|
||||
context.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private String xml(String configName) {
|
||||
return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
|
||||
}
|
||||
}
|
||||
+4
@@ -80,6 +80,7 @@ public class FormLoginBeanDefinitionParserTests {
|
||||
+ " </p>\n"
|
||||
+ " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
|
||||
+ " </form>\n"
|
||||
+ "</div>\n"
|
||||
+ "</body></html>";
|
||||
|
||||
this.mvc.perform(get("/login")).andExpect(content().string(expectedContent));
|
||||
@@ -126,6 +127,7 @@ public class FormLoginBeanDefinitionParserTests {
|
||||
+ " </p>\n"
|
||||
+ " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
|
||||
+ " </form>\n"
|
||||
+ "</div>\n"
|
||||
+ "</body></html>";
|
||||
|
||||
this.mvc.perform(get("/login")).andExpect(content().string(expectedContent));
|
||||
@@ -171,6 +173,7 @@ public class FormLoginBeanDefinitionParserTests {
|
||||
+ " </p>\n"
|
||||
+ " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
|
||||
+ " </form>\n"
|
||||
+ "</div>\n"
|
||||
+ "</body></html>";
|
||||
|
||||
this.mvc.perform(get("/login")).andExpect(content().string(expectedContent));
|
||||
@@ -214,6 +217,7 @@ public class FormLoginBeanDefinitionParserTests {
|
||||
+ " </p>\n"
|
||||
+ " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
|
||||
+ " </form>\n"
|
||||
+ "</div>\n"
|
||||
+ "</body></html>";
|
||||
|
||||
this.mvc.perform(get("/login")).andExpect(content().string(expectedContent));
|
||||
|
||||
+10
-10
@@ -15,15 +15,7 @@
|
||||
*/
|
||||
package org.springframework.security.config.http;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.same;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.spy;
|
||||
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -32,14 +24,14 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
|
||||
import org.springframework.security.config.util.InMemoryXmlApplicationContext;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
@@ -47,12 +39,20 @@ import org.springframework.security.web.context.HttpRequestResponseHolder;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.same;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.spy;
|
||||
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({ ReflectionUtils.class, Method.class })
|
||||
@PowerMockIgnore({ "org.w3c.dom.*", "org.xml.sax.*", "org.apache.xerces.*", "javax.xml.parsers.*" })
|
||||
public class SessionManagementConfigServlet31Tests {
|
||||
private static final String XML_AUTHENTICATION_MANAGER = "<authentication-manager>"
|
||||
+ " <authentication-provider>" + " <user-service>"
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util"
|
||||
xmlns:security="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
|
||||
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
|
||||
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd
|
||||
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
|
||||
">
|
||||
|
||||
<security:http pattern="/foo/**">
|
||||
<security:intercept-url pattern="/**" access="hasRole('FOO')" />
|
||||
<security:http-basic/>
|
||||
</security:http>
|
||||
|
||||
<security:http pattern="/bar/**">
|
||||
<security:intercept-url pattern="/**" access="hasRole('BAR')" />
|
||||
<security:http-basic/>
|
||||
</security:http>
|
||||
|
||||
<security:authentication-manager >
|
||||
<security:authentication-provider>
|
||||
<security:user-service>
|
||||
<security:user name="gnu" password="{noop}gnat" authorities="ROLE_FOO" />
|
||||
</security:user-service>
|
||||
</security:authentication-provider>
|
||||
</security:authentication-manager>
|
||||
|
||||
</beans>
|
||||
+3
-1
@@ -112,7 +112,9 @@ class MethodSecurityEvaluationContext extends StandardEvaluationContext {
|
||||
}
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
super.setVariable(paramNames[i], args[i]);
|
||||
if (paramNames[i] != null) {
|
||||
setVariable(paramNames[i], args[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
* 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,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -157,7 +156,9 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
||||
throws AuthenticationException {
|
||||
Class<? extends Authentication> toTest = authentication.getClass();
|
||||
AuthenticationException lastException = null;
|
||||
AuthenticationException parentException = null;
|
||||
Authentication result = null;
|
||||
Authentication parentResult = null;
|
||||
boolean debug = logger.isDebugEnabled();
|
||||
|
||||
for (AuthenticationProvider provider : getProviders()) {
|
||||
@@ -196,7 +197,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
||||
if (result == null && parent != null) {
|
||||
// Allow the parent to try.
|
||||
try {
|
||||
result = parent.authenticate(authentication);
|
||||
result = parentResult = parent.authenticate(authentication);
|
||||
}
|
||||
catch (ProviderNotFoundException e) {
|
||||
// ignore as we will throw below if no other exception occurred prior to
|
||||
@@ -205,7 +206,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
||||
// handled the request
|
||||
}
|
||||
catch (AuthenticationException e) {
|
||||
lastException = e;
|
||||
lastException = parentException = e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +218,11 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
||||
((CredentialsContainer) result).eraseCredentials();
|
||||
}
|
||||
|
||||
eventPublisher.publishAuthenticationSuccess(result);
|
||||
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
|
||||
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
|
||||
if (parentResult == null) {
|
||||
eventPublisher.publishAuthenticationSuccess(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -230,7 +235,11 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
|
||||
"No AuthenticationProvider found for {0}"));
|
||||
}
|
||||
|
||||
prepareException(lastException, authentication);
|
||||
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
|
||||
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
|
||||
if (parentException == null) {
|
||||
prepareException(lastException, authentication);
|
||||
}
|
||||
|
||||
throw lastException;
|
||||
}
|
||||
|
||||
+1
-11
@@ -132,17 +132,7 @@ public class SessionRegistryImpl implements SessionRegistry,
|
||||
sessionIds.put(sessionId,
|
||||
new SessionInformation(principal, sessionId, new Date()));
|
||||
|
||||
Set<String> sessionsUsedByPrincipal = principals.get(principal);
|
||||
|
||||
if (sessionsUsedByPrincipal == null) {
|
||||
sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
|
||||
Set<String> prevSessionsUsedByPrincipal = principals.putIfAbsent(principal,
|
||||
sessionsUsedByPrincipal);
|
||||
if (prevSessionsUsedByPrincipal != null) {
|
||||
sessionsUsedByPrincipal = prevSessionsUsedByPrincipal;
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> sessionsUsedByPrincipal = principals.computeIfAbsent(principal, key -> new CopyOnWriteArraySet<>());
|
||||
sessionsUsedByPrincipal.add(sessionId);
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
|
||||
+19
-4
@@ -57,6 +57,7 @@ import java.util.Set;
|
||||
* mapper.registerModule(new CoreJackson2Module());
|
||||
* mapper.registerModule(new CasJackson2Module());
|
||||
* mapper.registerModule(new WebJackson2Module());
|
||||
* mapper.registerModule(new WebServletJackson2Module());
|
||||
* </pre>
|
||||
*
|
||||
* @author Jitendra Singh.
|
||||
@@ -70,6 +71,8 @@ public final class SecurityJackson2Modules {
|
||||
"org.springframework.security.cas.jackson2.CasJackson2Module",
|
||||
"org.springframework.security.web.jackson2.WebJackson2Module"
|
||||
);
|
||||
private static final String webServletJackson2ModuleClass =
|
||||
"org.springframework.security.web.jackson2.WebServletJackson2Module";
|
||||
|
||||
private SecurityJackson2Modules() {
|
||||
}
|
||||
@@ -109,14 +112,26 @@ public final class SecurityJackson2Modules {
|
||||
public static List<Module> getModules(ClassLoader loader) {
|
||||
List<Module> modules = new ArrayList<>();
|
||||
for (String className : securityJackson2ModuleClasses) {
|
||||
Module module = loadAndGetInstance(className, loader);
|
||||
if (module != null) {
|
||||
modules.add(module);
|
||||
}
|
||||
addToModulesList(loader, modules, className);
|
||||
}
|
||||
if (ClassUtils.isPresent("javax.servlet.http.Cookie", loader)) {
|
||||
addToModulesList(loader, modules, webServletJackson2ModuleClass);
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loader the ClassLoader to use
|
||||
* @param modules list of the modules to add
|
||||
* @param className name of the class to instantiate
|
||||
*/
|
||||
private static void addToModulesList(ClassLoader loader, List<Module> modules, String className) {
|
||||
Module module = loadAndGetInstance(className, loader);
|
||||
if (module != null) {
|
||||
modules.add(module);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TypeResolverBuilder that performs whitelisting.
|
||||
* @return a TypeResolverBuilder that performs whitelisting.
|
||||
|
||||
+3
-2
@@ -41,6 +41,7 @@ import org.springframework.security.core.GrantedAuthority;
|
||||
*
|
||||
* @author Jitendra Singh
|
||||
* @author Greg Turnquist
|
||||
* @author Onur Kagan Ozcan
|
||||
* @see UsernamePasswordAuthenticationTokenMixin
|
||||
* @since 4.2
|
||||
*/
|
||||
@@ -69,7 +70,7 @@ class UsernamePasswordAuthenticationTokenDeserializer extends JsonDeserializer<U
|
||||
}
|
||||
JsonNode credentialsNode = readJsonNode(jsonNode, "credentials");
|
||||
Object credentials;
|
||||
if (credentialsNode.isNull()) {
|
||||
if (credentialsNode.isNull() || credentialsNode.isMissingNode()) {
|
||||
credentials = null;
|
||||
} else {
|
||||
credentials = credentialsNode.asText();
|
||||
@@ -83,7 +84,7 @@ class UsernamePasswordAuthenticationTokenDeserializer extends JsonDeserializer<U
|
||||
token = new UsernamePasswordAuthenticationToken(principal, credentials);
|
||||
}
|
||||
JsonNode detailsNode = readJsonNode(jsonNode, "details");
|
||||
if (detailsNode.isNull()) {
|
||||
if (detailsNode.isNull() || detailsNode.isMissingNode()) {
|
||||
token.setDetails(null);
|
||||
} else {
|
||||
token.setDetails(detailsNode);
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ public class InMemoryUserDetailsManager implements UserDetailsManager,
|
||||
@Override
|
||||
public UserDetails updatePassword(UserDetails user, String newPassword) {
|
||||
String username = user.getUsername();
|
||||
MutableUserDetails mutableUser = this.users.get(username);
|
||||
MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
|
||||
mutableUser.setPassword(newPassword);
|
||||
return mutableUser;
|
||||
}
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsMa
|
||||
// GroupManager SQL
|
||||
public static final String DEF_FIND_GROUPS_SQL = "select group_name from groups";
|
||||
public static final String DEF_FIND_USERS_IN_GROUP_SQL = "select username from group_members gm, groups g "
|
||||
+ "where gm.group_id = g.id" + " and g.group_name = ?";
|
||||
+ "where gm.group_id = g.id and g.group_name = ?";
|
||||
public static final String DEF_INSERT_GROUP_SQL = "insert into groups (group_name) values (?)";
|
||||
public static final String DEF_FIND_GROUP_ID_SQL = "select id from groups where group_name = ?";
|
||||
public static final String DEF_INSERT_GROUP_AUTHORITY_SQL = "insert into group_authorities (group_id, authority) values (?,?)";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
AbstractAccessDecisionManager.accessDenied=\u4E0D\u5141\u8BB8\u8BBF\u95EE
|
||||
AbstractLdapAuthenticationProvider.emptyPassword=\u574F\u7684\u51ED\u8BC1
|
||||
AbstractLdapAuthenticationProvider.emptyPassword=\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef
|
||||
AbstractSecurityInterceptor.authenticationNotFound=\u672A\u5728SecurityContext\u4E2D\u67E5\u627E\u5230\u8BA4\u8BC1\u5BF9\u8C61
|
||||
AbstractUserDetailsAuthenticationProvider.badCredentials=\u574F\u7684\u51ED\u8BC1
|
||||
AbstractUserDetailsAuthenticationProvider.badCredentials=\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef
|
||||
AbstractUserDetailsAuthenticationProvider.credentialsExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F
|
||||
AbstractUserDetailsAuthenticationProvider.disabled=\u7528\u6237\u5DF2\u5931\u6548
|
||||
AbstractUserDetailsAuthenticationProvider.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u671F
|
||||
@@ -13,8 +13,8 @@ AccountStatusUserDetailsChecker.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u67
|
||||
AccountStatusUserDetailsChecker.locked=\u7528\u6237\u5E10\u53F7\u5DF2\u88AB\u9501\u5B9A
|
||||
AclEntryAfterInvocationProvider.noPermission=\u7ED9\u5B9A\u7684Authentication\u5BF9\u8C61({0})\u6839\u672C\u65E0\u6743\u64CD\u63A7\u9886\u57DF\u5BF9\u8C61({1})
|
||||
AnonymousAuthenticationProvider.incorrectKey=\u5C55\u793A\u7684AnonymousAuthenticationToken\u4E0D\u542B\u6709\u9884\u671F\u7684key
|
||||
BindAuthenticator.badCredentials=\u574F\u7684\u51ED\u8BC1
|
||||
BindAuthenticator.emptyPassword=\u574F\u7684\u51ED\u8BC1
|
||||
BindAuthenticator.badCredentials=\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef
|
||||
BindAuthenticator.emptyPassword=\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef
|
||||
CasAuthenticationProvider.incorrectKey=\u5C55\u793A\u7684CasAuthenticationToken\u4E0D\u542B\u6709\u9884\u671F\u7684key
|
||||
CasAuthenticationProvider.noServiceTicket=\u672A\u80FD\u591F\u6B63\u786E\u63D0\u4F9B\u5F85\u9A8C\u8BC1\u7684CAS\u670D\u52A1\u7968\u6839
|
||||
ConcurrentSessionControlAuthenticationStrategy.exceededAllowed=\u5DF2\u7ECF\u8D85\u8FC7\u4E86\u5F53\u524D\u4E3B\u4F53({0})\u88AB\u5141\u8BB8\u7684\u6700\u5927\u4F1A\u8BDD\u6570\u91CF
|
||||
@@ -30,14 +30,14 @@ DigestAuthenticationFilter.nonceNotTwoTokens=Nonce\u5E94\u8BE5\u7531\u4E24\u90E8
|
||||
DigestAuthenticationFilter.usernameNotFound=\u7528\u6237\u540D{0}\u672A\u627E\u5230
|
||||
JdbcDaoImpl.noAuthority=\u6CA1\u6709\u4E3A\u7528\u6237{0}\u6307\u5B9A\u89D2\u8272
|
||||
JdbcDaoImpl.notFound=\u672A\u627E\u5230\u7528\u6237{0}
|
||||
LdapAuthenticationProvider.badCredentials=\u574F\u7684\u51ED\u8BC1
|
||||
LdapAuthenticationProvider.badCredentials=\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef
|
||||
LdapAuthenticationProvider.credentialsExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F
|
||||
LdapAuthenticationProvider.disabled=\u7528\u6237\u5DF2\u5931\u6548
|
||||
LdapAuthenticationProvider.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u671F
|
||||
LdapAuthenticationProvider.locked=\u7528\u6237\u5E10\u53F7\u5DF2\u88AB\u9501\u5B9A
|
||||
LdapAuthenticationProvider.emptyUsername=\u7528\u6237\u540D\u4E0D\u5141\u8BB8\u4E3A\u7A7A
|
||||
LdapAuthenticationProvider.onlySupports=\u4EC5\u4EC5\u652F\u6301UsernamePasswordAuthenticationToken
|
||||
PasswordComparisonAuthenticator.badCredentials=\u574F\u7684\u51ED\u8BC1
|
||||
PasswordComparisonAuthenticator.badCredentials=\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef
|
||||
#PersistentTokenBasedRememberMeServices.cookieStolen=Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.
|
||||
ProviderManager.providerNotFound=\u672A\u67E5\u627E\u5230\u9488\u5BF9{0}\u7684AuthenticationProvider
|
||||
RememberMeAuthenticationProvider.incorrectKey=\u5C55\u793ARememberMeAuthenticationToken\u4E0D\u542B\u6709\u9884\u671F\u7684key
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.access.expression.method;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
|
||||
/**
|
||||
* @author shabarijonnalagadda
|
||||
*
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class MethodSecurityEvaluationContextTests {
|
||||
@Mock
|
||||
private ParameterNameDiscoverer paramNameDiscoverer;
|
||||
@Mock
|
||||
private Authentication authentication;
|
||||
@Mock
|
||||
private MethodInvocation methodInvocation;
|
||||
|
||||
@Test
|
||||
public void lookupVariableWhenParameterNameNullThenNotSet() {
|
||||
Class<String> type = String.class;
|
||||
Method method = ReflectionUtils.findMethod(String.class, "contains", CharSequence.class);
|
||||
doReturn(new String[] {null}).when(paramNameDiscoverer).getParameterNames(method);
|
||||
doReturn(new Object[]{null}).when(methodInvocation).getArguments();
|
||||
doReturn(type).when(methodInvocation).getThis();
|
||||
doReturn(method).when(methodInvocation).getMethod();
|
||||
NotNullVariableMethodSecurityEvaluationContext context= new NotNullVariableMethodSecurityEvaluationContext(authentication, methodInvocation, paramNameDiscoverer);
|
||||
context.lookupVariable("testVariable");
|
||||
}
|
||||
|
||||
private static class NotNullVariableMethodSecurityEvaluationContext
|
||||
extends MethodSecurityEvaluationContext {
|
||||
|
||||
public NotNullVariableMethodSecurityEvaluationContext(Authentication auth, MethodInvocation mi,
|
||||
ParameterNameDiscoverer parameterNameDiscoverer) {
|
||||
super(auth, mi, parameterNameDiscoverer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVariable(String name, @Nullable Object value) {
|
||||
if ( name == null ) {
|
||||
throw new IllegalArgumentException("name should not be null");
|
||||
}
|
||||
else {
|
||||
super.setVariable(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-8
@@ -16,18 +16,20 @@
|
||||
|
||||
package org.springframework.security.authentication;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests {@link ProviderManager}.
|
||||
*
|
||||
@@ -257,7 +259,6 @@ public class ProviderManagerTests {
|
||||
catch (BadCredentialsException e) {
|
||||
assertThat(e).isSameAs(expected);
|
||||
}
|
||||
verify(publisher).publishAuthenticationFailure(expected, authReq);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -298,6 +299,32 @@ public class ProviderManagerTests {
|
||||
}
|
||||
}
|
||||
|
||||
// gh-6281
|
||||
@Test
|
||||
public void authenticateWhenFailsInParentAndPublishesThenChildDoesNotPublish() {
|
||||
BadCredentialsException badCredentialsExParent = new BadCredentialsException("Bad Credentials in parent");
|
||||
ProviderManager parentMgr = new ProviderManager(
|
||||
Collections.singletonList(createProviderWhichThrows(badCredentialsExParent)));
|
||||
ProviderManager childMgr = new ProviderManager(Collections.singletonList(createProviderWhichThrows(
|
||||
new BadCredentialsException("Bad Credentials in child"))), parentMgr);
|
||||
|
||||
AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class);
|
||||
parentMgr.setAuthenticationEventPublisher(publisher);
|
||||
childMgr.setAuthenticationEventPublisher(publisher);
|
||||
|
||||
final Authentication authReq = mock(Authentication.class);
|
||||
|
||||
try {
|
||||
childMgr.authenticate(authReq);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (BadCredentialsException e) {
|
||||
assertThat(e).isSameAs(badCredentialsExParent);
|
||||
}
|
||||
verify(publisher).publishAuthenticationFailure(badCredentialsExParent, authReq); // Parent publishes
|
||||
verifyNoMoreInteractions(publisher); // Child should not publish (duplicate event)
|
||||
}
|
||||
|
||||
private AuthenticationProvider createProviderWhichThrows(
|
||||
final AuthenticationException e) {
|
||||
AuthenticationProvider provider = mock(AuthenticationProvider.class);
|
||||
|
||||
+20
-1
@@ -29,11 +29,16 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static com.fasterxml.jackson.annotation.JsonInclude.Include.ALWAYS;
|
||||
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_ABSENT;
|
||||
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
|
||||
import static com.fasterxml.jackson.annotation.JsonInclude.Value.construct;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Jitendra Singh
|
||||
* @author Greg Turnquist
|
||||
* @author Onur Kagan Ozcan
|
||||
* @since 4.2
|
||||
*/
|
||||
public class UsernamePasswordAuthenticationTokenMixinTests extends AbstractMixinTests {
|
||||
@@ -163,6 +168,20 @@ public class UsernamePasswordAuthenticationTokenMixinTests extends AbstractMixin
|
||||
assertThat(deserialized).isEqualTo(original);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializingThenDeserializingWithConfiguredObjectMapperShouldWork() throws IOException {
|
||||
// given
|
||||
this.mapper.setDefaultPropertyInclusion(construct(ALWAYS, NON_NULL)).setSerializationInclusion(NON_ABSENT);
|
||||
UsernamePasswordAuthenticationToken original = new UsernamePasswordAuthenticationToken("Frodo", null);
|
||||
|
||||
// when
|
||||
String serialized = this.mapper.writeValueAsString(original);
|
||||
UsernamePasswordAuthenticationToken deserialized =
|
||||
this.mapper.readValue(serialized, UsernamePasswordAuthenticationToken.class);
|
||||
|
||||
// then
|
||||
assertThat(deserialized).isEqualTo(original);
|
||||
}
|
||||
|
||||
private UsernamePasswordAuthenticationToken createToken() {
|
||||
User user = createDefaultUser();
|
||||
|
||||
+12
@@ -18,6 +18,7 @@ package org.springframework.security.provisioning;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.core.userdetails.PasswordEncodedUser;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
@@ -37,4 +38,15 @@ public class InMemoryUserDetailsManagerTests {
|
||||
this.manager.updatePassword(this.user, newPassword);
|
||||
assertThat(this.manager.loadUserByUsername(this.user.getUsername()).getPassword()).isEqualTo(newPassword);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changePasswordWhenUsernameIsNotInLowercase() {
|
||||
UserDetails userNotLowerCase = User.withUserDetails(PasswordEncodedUser.user())
|
||||
.username("User")
|
||||
.build();
|
||||
|
||||
String newPassword = "newPassword";
|
||||
this.manager.updatePassword(userNotLowerCase, newPassword);
|
||||
assertThat(this.manager.loadUserByUsername(userNotLowerCase.getUsername()).getPassword()).isEqualTo(newPassword);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ In order to use Spring Security you must add the necessary dependencies. For the
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.thymeleaf.extras</groupId>
|
||||
<artifactId>thymeleaf-extras-springsecurity4</artifactId> <1>
|
||||
<artifactId>thymeleaf-extras-springsecurity5</artifactId> <1>
|
||||
<version>2.1.2.RELEASE</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -32,7 +32,7 @@ Now that we have authenticated, let's update the application to display the user
|
||||
[source,html]
|
||||
----
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
<head>
|
||||
<title>Hello Spring Security</title>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -49,7 +49,7 @@ If you wish to override the Spring Security version, you may do so by providing
|
||||
----
|
||||
<properties>
|
||||
<!-- ... -->
|
||||
<spring-security.version>{spring-security-version}</spring.security.version>
|
||||
<spring-security.version>{spring-security-version}</spring-security.version>
|
||||
</dependencies>
|
||||
----
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ spring:
|
||||
github:
|
||||
client-id: replace-with-client-id
|
||||
client-secret: replace-with-client-secret
|
||||
scopes: read:user,public_repo
|
||||
scope: read:user,public_repo
|
||||
----
|
||||
|
||||
You will need to replace the `client-id` and `client-secret` with values registered with GitHub.
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
[[webflux-oauth2-resource-server]]
|
||||
= OAuth2 Resource Server
|
||||
|
||||
Spring Security provides OAuth2 Resource Server support with JWT tokens.
|
||||
Spring Security supports protecting endpoints using https://tools.ietf.org/html/rfc7519[JWT]-encoded OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens].
|
||||
|
||||
This is handy in circumstances where an application has federated its authority management out to an https://tools.ietf.org/html/rfc6749[authorization server] (for example, Okta or Ping Identity).
|
||||
This authorization server can be consulted by Resource Servers to validate authority when serving requests.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
A complete working example can be found in {gh-samples-url}/boot/oauth2resourceserver-webflux[*OAuth 2.0 Resource Server WebFlux sample*].
|
||||
====
|
||||
|
||||
The first step is to expose a `ReactiveJwtDecoder` as a `@Bean`.
|
||||
In a Spring Boot application this can be done using:
|
||||
== Dependencies
|
||||
|
||||
Most Resource Server support is collected into `spring-security-oauth2-resource-server`.
|
||||
However, the support for decoding and verifying JWTs is in `spring-security-oauth2-jose`, meaning that both are necessary in order to have a working resource server that supports JWT-encoded Bearer Tokens.
|
||||
|
||||
[[webflux-oauth2-resource-server-minimal-configuration]]
|
||||
== Minimal Configuration
|
||||
|
||||
When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a resource server consists of two basic steps.
|
||||
First, include the needed dependencies and second, indicate the location of the authorization server.
|
||||
|
||||
=== Specify the Authorization Server
|
||||
|
||||
In a Spring Boot application, to specify which authorization server to use, simply do:
|
||||
|
||||
[source,yml]
|
||||
----
|
||||
@@ -19,15 +33,108 @@ spring:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
issuer-uri: https://idp.example.com/auth/realms/demo
|
||||
issuer-uri: https://idp.example.com
|
||||
----
|
||||
|
||||
The `issuer-uri` instructs Spring Security to leverage the endpoint at `https://idp.example.com/auth/realms/demo/.well-known/openid-configuration` to discover the configuration.
|
||||
The above is all that is necessary to get a minimal Resource Server configured.
|
||||
When new keys are made available, Spring Security will automatically rotate the keys used to validate the JWT tokens.
|
||||
Where `https://idp.example.com` is the value contained in the `iss` claim for JWT tokens that the authorization server will issue.
|
||||
Resource Server will use this property to further self-configure, discover the authorization server's public keys, and subsequently validate incoming JWTs.
|
||||
|
||||
By default each scope is mapped to an authority with the prefix `SCOPE_`.
|
||||
For example, the following requires the scope of `message:read` for any URL that starts with `/messages/`.
|
||||
[NOTE]
|
||||
To use the `issuer-uri` property, it must also be true that `https://idp.example.com/.well-known/openid-configuration` is a supported endpoint for the authorization server.
|
||||
This endpoint is referred to as a https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Provider Configuration] endpoint.
|
||||
|
||||
And that's it!
|
||||
|
||||
=== Startup Expectations
|
||||
|
||||
When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens.
|
||||
|
||||
It achieves this through a deterministic startup process:
|
||||
|
||||
1. Hit the Provider Configuration endpoint, `https://the.issuer.location/.well-known/openid-configuration`, processing the response for the `jwks_url` property
|
||||
2. Configure the validation strategy to query `jwks_url` for valid public keys
|
||||
3. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`.
|
||||
|
||||
A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up.
|
||||
|
||||
[NOTE]
|
||||
If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail.
|
||||
|
||||
=== Runtime Expectations
|
||||
|
||||
Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header:
|
||||
|
||||
[source,html]
|
||||
----
|
||||
GET / HTTP/1.1
|
||||
Authorization: Bearer some-token-value # Resource Server will process this
|
||||
----
|
||||
|
||||
So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.
|
||||
|
||||
Given a well-formed JWT token, Resource Server will:
|
||||
|
||||
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header
|
||||
2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and
|
||||
3. Map each scope to an authority with the prefix `SCOPE_`.
|
||||
|
||||
[NOTE]
|
||||
As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens.
|
||||
|
||||
The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present.
|
||||
|
||||
<<webflux-oauth2-resource-server-jwkseturi,How to Configure without Tying Resource Server startup to an authorization server's availability>>
|
||||
|
||||
<<webflux-oauth2-resource-server-sans-boot,How to Configure without Spring Boot>>
|
||||
|
||||
[[webflux-oauth2-resource-server-jwkseturi]]
|
||||
=== Specifying the Authorization Server JWK Set Uri Directly
|
||||
|
||||
If the authorization server doesn't support the Provider Configuration endpoint, or if Resource Server must be able to start up independently from the authorization server, then `issuer-uri` can be exchanged for `jwk-set-uri`:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
jwk-set-uri: https://idp.example.com/.well-known/jwks.json
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
The JWK Set uri is not standardized, but can typically be found in the authorization server's documentation
|
||||
|
||||
Consequently, Resource Server will not ping the authorization server at startup.
|
||||
However, it will also no longer validate the `iss` claim in the JWT (since Resource Server no longer knows what the issuer value should be).
|
||||
|
||||
[NOTE]
|
||||
This property can also be supplied directly on the <<webflux-oauth2-resource-server-jwkseturi-dsl,DSL>>.
|
||||
|
||||
[[webflux-oauth2-resource-server-sans-boot]]
|
||||
=== Overriding or Replacing Boot Auto Configuration
|
||||
|
||||
There are two `@Bean` s that Spring Boot generates on Resource Server's behalf.
|
||||
|
||||
The first is a `SecurityWebFilterChain` that configures the app as a resource server:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt();
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
If the application doesn't expose a `SecurityWebFilterChain` bean, then Spring Boot will expose the above default one.
|
||||
|
||||
Replacing this is as simple as exposing the bean within the application:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@@ -44,4 +151,247 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
}
|
||||
----
|
||||
|
||||
The above requires the scope of `message:read` for any URL that starts with `/messages/`.
|
||||
|
||||
Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration.
|
||||
|
||||
For example, the second `@Bean` Spring Boot creates is a `ReactiveJwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public ReactiveJwtDecoder jwtDecoder() {
|
||||
return ReactiveJwtDecoders.fromOidcIssuerLocation(issuerUri);
|
||||
}
|
||||
----
|
||||
|
||||
If the application doesn't expose a `ReactiveJwtDecoder` bean, then Spring Boot will expose the above default one.
|
||||
|
||||
And its configuration can be overridden using `jwkSetUri()` or replaced using `decoder()`.
|
||||
|
||||
[[webflux-oauth2-resource-server-jwkseturi-dsl]]
|
||||
==== Using `jwkSetUri()`
|
||||
|
||||
An authorization server's JWK Set Uri can be configured <<webflux-oauth2-resource-server-jwkseturi,as a configuration property>> or it can be supplied in the DSL:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt()
|
||||
.jwkSetUri("https://idp.example.com/.well-known/jwks.json");
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
Using `jwkSetUri()` takes precedence over any configuration property.
|
||||
|
||||
[[webflux-oauth2-resource-server-decoder-dsl]]
|
||||
==== Using `decoder()`
|
||||
|
||||
More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt()
|
||||
.decoder(myCustomDecoder());
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
This is handy when deeper configuration, like <<webflux-oauth2-resource-server-validation,validation>>, is necessary.
|
||||
|
||||
[[webflux-oauth2-resource-server-decoder-bean]]
|
||||
==== Exposing a `ReactiveJwtDecoder` `@Bean`
|
||||
|
||||
Or, exposing a `ReactiveJwtDecoder` `@Bean` has the same effect as `decoder()`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
return new NimbusReactiveJwtDecoder(jwkSetUri);
|
||||
}
|
||||
----
|
||||
|
||||
[[webflux-oauth2-resource-server-authorization]]
|
||||
=== Configuring Authorization
|
||||
|
||||
A JWT that is issued from an OAuth 2.0 Authorization Server will typically either have a `scope` or `scp` attribute, indicating the scopes (or authorities) it's been granted, for example:
|
||||
|
||||
`{ ..., "scope" : "messages contacts"}`
|
||||
|
||||
When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the string "SCOPE_".
|
||||
|
||||
This means that to protect an endpoint or method with a scope derived from a JWT, the corresponding expressions should include this prefix:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
|
||||
.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt();
|
||||
return http.build();
|
||||
}
|
||||
----
|
||||
|
||||
Or similarly with method security:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@PreAuthorize("hasAuthority('SCOPE_messages')")
|
||||
public List<Message> getMessages(...) {}
|
||||
----
|
||||
|
||||
[[webflux-oauth2-resource-server-authorization-extraction]]
|
||||
==== Extracting Authorities Manually
|
||||
|
||||
However, there are a number of circumstances where this default is insufficient.
|
||||
For example, some authorization servers don't use the `scope` attribute, but instead have their own custom attribute.
|
||||
Or, at other times, the resource server may need to adapt the attribute or a composition of attributes into internalized authorities.
|
||||
|
||||
To this end, the DSL exposes `jwtAuthenticationConverter()`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt()
|
||||
.jwtAuthenticationConverter(grantedAuthoritiesExtractor());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
|
||||
GrantedAuthoritiesExtractor extractor = new GrantedAuthoritiesExtractor();
|
||||
return new ReactiveJwtAuthenticationConverterAdapter(extractor);
|
||||
}
|
||||
----
|
||||
|
||||
which is responsible for converting a `Jwt` into an `Authentication`.
|
||||
|
||||
We can override this quite simply to alter the way granted authorities are derived:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
static class GrantedAuthoritiesExtractor extends JwtAuthenticationConverter {
|
||||
protected Collection<GrantedAuthorities> extractAuthorities(Jwt jwt) {
|
||||
Collection<String> authorities = (Collection<String>)
|
||||
jwt.getClaims().get("mycustomclaim");
|
||||
|
||||
return authorities.stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter<Jwt, Mono<AbstractAuthenticationToken>>`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
|
||||
public AbstractAuthenticationToken convert(Jwt jwt) {
|
||||
return Mono.just(jwt).map(this::doConversion);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[webflux-oauth2-resource-server-validation]]
|
||||
=== Configuring Validation
|
||||
|
||||
Using <<webflux-oauth2-resource-server-minimal-configuration,minimal Spring Boot configuration>>, indicating the authorization server's issuer uri, Resource Server will default to verifying the `iss` claim as well as the `exp` and `nbf` timestamp claims.
|
||||
|
||||
In circumstances where validation needs to be customized, Resource Server ships with two standard validators and also accepts custom `OAuth2TokenValidator` instances.
|
||||
|
||||
[[webflux-oauth2-resource-server-validation-clockskew]]
|
||||
==== Customizing Timestamp Validation
|
||||
|
||||
JWT's typically have a window of validity, with the start of the window indicated in the `nbf` claim and the end indicated in the `exp` claim.
|
||||
|
||||
However, every server can experience clock drift, which can cause tokens to appear expired to one server, but not to another.
|
||||
This can cause some implementation heartburn as the number of collaborating servers increases in a distributed system.
|
||||
|
||||
Resource Server uses `JwtTimestampValidator` to verify a token's validity window, and it can be configured with a `clockSkew` to alleviate the above problem:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
|
||||
ReactiveJwtDecoders.withOidcIssuerLocation(issuerUri);
|
||||
|
||||
OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
|
||||
new JwtTimestampValidator(Duration.ofSeconds(60)),
|
||||
new IssuerValidator(issuerUri));
|
||||
|
||||
jwtDecoder.setJwtValidator(withClockSkew);
|
||||
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
By default, Resource Server configures a clock skew of 30 seconds.
|
||||
|
||||
[[webflux-oauth2-resource-server-validation-custom]]
|
||||
==== Configuring a Custom Validator
|
||||
|
||||
Adding a check for the `aud` claim is simple with the `OAuth2TokenValidator` API:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
|
||||
OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
|
||||
|
||||
public OAuth2TokenValidatorResult validate(Jwt jwt) {
|
||||
if (jwt.getAudience().contains("messaging")) {
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
} else {
|
||||
return OAuth2TokenValidatorResult.failure(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Then, to add into a resource server, it's a matter of specifying the `ReactiveJwtDecoder` instance:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
ReactiveJwtDecoder jwtDecoder() {
|
||||
NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
|
||||
ReactiveJwtDecoders.withOidcIssuerLocation(issuerUri);
|
||||
|
||||
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
|
||||
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
|
||||
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
|
||||
|
||||
jwtDecoder.setJwtValidator(withAudience);
|
||||
|
||||
return jwtDecoder;
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
|
||||
[[webclient-implicit]]
|
||||
== Implicit OAuth2AuthorizedClient
|
||||
|
||||
If we set `defaultOAuth2AuthorizedClient` to `true`in our setup and the user authenticated with oauth2Login (i.e. OIDC), then the current authentication is used to automatically provide the access token.
|
||||
If we set `defaultOAuth2AuthorizedClient` to `true` in our setup and the user authenticated with oauth2Login (i.e. OIDC), then the current authentication is used to automatically provide the access token.
|
||||
Alternatively, if we set `defaultClientRegistrationId` to a valid `ClientRegistration` id, that registration is used to provide the access token.
|
||||
This is convenient, but in environments where not all endpoints should get the access token, it is dangerous (you might provide the wrong access token to an endpoint).
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Spring Security has added Jackson Support for persisting Spring Security related classes.
|
||||
This can improve the performance of serializing Spring Security related classes when working with distributed sessions (i.e. session replication, Spring Session, etc).
|
||||
|
||||
To use it, register the `JacksonJacksonModules.getModules(ClassLoader)` as http://wiki.fasterxml.com/JacksonFeatureModules[Jackson Modules].
|
||||
To use it, register the `SecurityJackson2Modules.getModules(ClassLoader)` as http://wiki.fasterxml.com/JacksonFeatureModules[Jackson Modules].
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
||||
@@ -183,7 +183,7 @@ A typical configuration might look like this:
|
||||
----
|
||||
|
||||
Here we have four roles in a hierarchy `ROLE_ADMIN => ROLE_STAFF => ROLE_USER => ROLE_GUEST`.
|
||||
A user who is authenticated with `ROLE_ADMIN`, will behave as if they have all four roles when security constraints are evaluated against an `AccessDecisionManager` cconfigured with the above `RoleHierarchyVoter`.
|
||||
A user who is authenticated with `ROLE_ADMIN`, will behave as if they have all four roles when security constraints are evaluated against an `AccessDecisionManager` configured with the above `RoleHierarchyVoter`.
|
||||
The `>` symbol can be thought of as meaning "includes".
|
||||
|
||||
Role hierarchies offer a convenient means of simplifying the access-control configuration data for your application and/or reducing the number of authorities which you need to assign to a user.
|
||||
|
||||
@@ -397,6 +397,11 @@ Spring Security supports protecting endpoints using https://tools.ietf.org/html/
|
||||
This is handy in circumstances where an application has federated its authority management out to an https://tools.ietf.org/html/rfc6749[authorization server] (for example, Okta or Ping Identity).
|
||||
This authorization server can be consulted by Resource Servers to validate authority when serving requests.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
A complete working example can be found in {gh-samples-url}/boot/oauth2resourceserver[*OAuth 2.0 Resource Server Servlet sample*].
|
||||
====
|
||||
|
||||
=== Dependencies
|
||||
|
||||
Most Resource Server support is collected into `spring-security-oauth2-resource-server`.
|
||||
@@ -417,25 +422,27 @@ security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
issuer-uri: https://the.issuer.location
|
||||
issuer-uri: https://idp.example.com
|
||||
```
|
||||
|
||||
Where `https://the.issuer.location` is the value contained in the `iss` claim for JWT tokens that the authorization server will issue.
|
||||
Resource Server will use this property to further self-configure and subsequently validate incoming JWTs.
|
||||
Where `https://idp.example.com` is the value contained in the `iss` claim for JWT tokens that the authorization server will issue.
|
||||
Resource Server will use this property to further self-configure, discover the authorization server's public keys, and subsequently validate incoming JWTs.
|
||||
|
||||
[NOTE]
|
||||
To use the `issuer-uri` property, it must also be true that `https://the.issuer.location/.well-known/openid-configuration` is a supported endpoint for the authorization server.
|
||||
To use the `issuer-uri` property, it must also be true that `https://idp.example.com/.well-known/openid-configuration` is a supported endpoint for the authorization server.
|
||||
This endpoint is referred to as a https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Provider Configuration] endpoint.
|
||||
|
||||
And that's it!
|
||||
|
||||
==== Startup Expectations
|
||||
|
||||
When this property and these dependencies are used, Resource Server will automatically configure itself to validate JWT-encoded Bearer Tokens.
|
||||
|
||||
It achieves this through a deterministic startup process:
|
||||
|
||||
1. Hit the Provider Configuration endpoint, `https://the.issuer.location/.well-known/openid-configuration`, processing the response for the `jwks_url` property
|
||||
1. Hit the Provider Configuration endpoint, `https://idp.example.com/.well-known/openid-configuration`, processing the response for the `jwks_url` property
|
||||
2. Configure the validation strategy to query `jwks_url` for valid public keys
|
||||
3. Configure the validation strategy to validate each JWTs `iss` claim against `https://the.issuer.location`.
|
||||
3. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`.
|
||||
|
||||
A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up.
|
||||
|
||||
@@ -444,7 +451,7 @@ If the authorization server is down when Resource Server queries it (given appro
|
||||
|
||||
==== Runtime Expectations
|
||||
|
||||
Once the application is started up, Resource Server will attempt to process any request containing an `Authorizatization: Bearer` header:
|
||||
Once the application is started up, Resource Server will attempt to process any request containing an `Authorization: Bearer` header:
|
||||
|
||||
```http
|
||||
GET / HTTP/1.1
|
||||
@@ -453,10 +460,16 @@ Authorization: Bearer some-token-value # Resource Server will process this
|
||||
|
||||
So long as this scheme is indicated, Resource Server will attempt to process the request according to the Bearer Token specification.
|
||||
|
||||
Given a well-formed JWT token, Resource Server will validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim.
|
||||
It will also validate the signature against a public key obtained from the `jwks_url` endpoint and matched against the JWTs header.
|
||||
Given a well-formed JWT token, Resource Server will
|
||||
|
||||
The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` map's to the JWT's `sub` property, if one is present.
|
||||
1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header
|
||||
2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and
|
||||
3. Map each scope to an authority with the prefix `SCOPE_`.
|
||||
|
||||
[NOTE]
|
||||
As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens.
|
||||
|
||||
The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present.
|
||||
|
||||
From here, consider jumping to:
|
||||
|
||||
@@ -474,7 +487,7 @@ security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
jwk-set-uri: https://the.issuer.location/.well-known/jwks.json
|
||||
jwk-set-uri: https://idp.example.com/.well-known/jwks.json
|
||||
```
|
||||
|
||||
[NOTE]
|
||||
@@ -514,7 +527,7 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter
|
||||
protected void configure(HttpSecurity http) {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.mvcMatchers("/admin/**").hasAuthority("SCOPE_admin")
|
||||
.mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
@@ -524,6 +537,8 @@ public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter
|
||||
}
|
||||
```
|
||||
|
||||
The above requires the scope of `message:read` for any URL that starts with `/messages/`.
|
||||
|
||||
Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration.
|
||||
|
||||
For example, the second `@Bean` Spring Boot creates is a `JwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`:
|
||||
@@ -554,7 +569,7 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
||||
.and()
|
||||
.oauth2ResourceServer()
|
||||
.jwt()
|
||||
.jwkSetUri("https://the.issuer.location/.well-known/jwks.json");
|
||||
.jwkSetUri("https://idp.example.com/.well-known/jwks.json");
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -581,7 +596,7 @@ public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
|
||||
}
|
||||
```
|
||||
|
||||
This is handy when deeper configuration, like <<oauth2resourceserver-validator,validation>>, <<oauth2resourceserver-claimsetmapping,mapping>>, or <<oauth2resourceserver-timeouts,request timeouts>>, is necessary.
|
||||
This is handy when deeper configuration, like <<oauth2resourceserver-validation,validation>>, <<oauth2resourceserver-claimsetmapping,mapping>>, or <<oauth2resourceserver-timeouts,request timeouts>>, is necessary.
|
||||
|
||||
[[oauth2resourceserver-decoder-bean]]
|
||||
==== Exposing a `JwtDecoder` `@Bean`
|
||||
@@ -602,7 +617,7 @@ A JWT that is issued from an OAuth 2.0 Authorization Server will typically eithe
|
||||
|
||||
`{ ..., "scope" : "messages contacts"}`
|
||||
|
||||
When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the prefix "SCOPE_".
|
||||
When this is the case, Resource Server will attempt to coerce these scopes into a list of granted authorities, prefixing each scope with the string "SCOPE_".
|
||||
|
||||
This means that to protect an endpoint or method with a scope derived from a JWT, the corresponding expressions should include this prefix:
|
||||
|
||||
@@ -633,7 +648,7 @@ public List<Message> getMessages(...) {}
|
||||
==== Extracting Authorities Manually
|
||||
|
||||
However, there are a number of circumstances where this default is insufficient.
|
||||
For example, some authorization server's don't use the `scope` attribute, but instead have their own custom attribute.
|
||||
For example, some authorization servers don't use the `scope` attribute, but instead have their own custom attribute.
|
||||
Or, at other times, the resource server may need to adapt the attribute or a composition of attributes into internalized authorities.
|
||||
|
||||
To this end, the DSL exposes `jwtAuthenticationConverter()`:
|
||||
@@ -776,13 +791,12 @@ For these purposes, Resource Server supports mapping the JWT claim set with `Map
|
||||
|
||||
By default, `MappedJwtClaimSetConverter` will attempt to coerce claims into the following types:
|
||||
|
||||
|
||||
|============
|
||||
| Claim | Java Type
|
||||
| `aud` | `Collection<String>`
|
||||
| `exp` | `Instant`
|
||||
| `iat` | `Instant`
|
||||
| `iss` | `URL`
|
||||
| `iss` | `String`
|
||||
| `jti` | `String`
|
||||
| `nbf` | `Instant`
|
||||
| `sub` | `String`
|
||||
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
gaeVersion=1.9.66
|
||||
springBootVersion=2.1.0.M4
|
||||
version=5.1.2.BUILD-SNAPSHOT
|
||||
gaeVersion=1.9.71
|
||||
springBootVersion=2.1.2.RELEASE
|
||||
version=5.1.4.RELEASE
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
if (!project.hasProperty('reactorVersion')) {
|
||||
ext.reactorVersion = 'Californium-SR1'
|
||||
ext.reactorVersion = 'Californium-SR5'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springVersion')) {
|
||||
ext.springVersion = '5.1.1.RELEASE'
|
||||
ext.springVersion = '5.1.5.RELEASE'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springDataVersion')) {
|
||||
ext.springDataVersion = 'Lovelace-SR1'
|
||||
ext.springDataVersion = 'Lovelace-SR5'
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
@@ -17,17 +17,17 @@ dependencyManagement {
|
||||
mavenBom "org.springframework.data:spring-data-releasetrain:${springDataVersion}"
|
||||
}
|
||||
dependencies {
|
||||
dependency 'cglib:cglib-nodep:3.2.8'
|
||||
dependency 'com.squareup.okhttp3:mockwebserver:3.10.0'
|
||||
dependency 'cglib:cglib-nodep:3.2.10'
|
||||
dependency 'com.squareup.okhttp3:mockwebserver:3.12.1'
|
||||
dependency 'opensymphony:sitemesh:2.4.2'
|
||||
dependency 'org.gebish:geb-spock:0.10.0'
|
||||
dependency 'org.jasig.cas:cas-server-webapp:4.2.7'
|
||||
dependency 'org.powermock:powermock-api-mockito2:2.0.0-beta.5'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.0-beta.5'
|
||||
dependency 'org.powermock:powermock-core:2.0.0-beta.5'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.0-beta.5'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.0-beta.5'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.0-beta.5'
|
||||
dependency 'org.powermock:powermock-api-mockito2:2.0.0'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.0'
|
||||
dependency 'org.powermock:powermock-core:2.0.0'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.0'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.0'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.0'
|
||||
dependency 'org.python:jython:2.5.0'
|
||||
dependency 'org.spockframework:spock-core:1.0-groovy-2.4'
|
||||
dependency 'org.spockframework:spock-spring:1.0-groovy-2.4'
|
||||
@@ -40,9 +40,9 @@ dependencyManagement {
|
||||
dependency 'asm:asm:3.1'
|
||||
dependency 'ch.qos.logback:logback-classic:1.2.3'
|
||||
dependency 'ch.qos.logback:logback-core:1.2.3'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.9.7'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-core:2.9.7'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.7'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-annotations:2.9.8'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-core:2.9.8'
|
||||
dependency 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
|
||||
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.64'
|
||||
@@ -56,11 +56,11 @@ dependencyManagement {
|
||||
dependency 'com.nimbusds:lang-tag:1.4.3'
|
||||
dependency 'com.nimbusds:nimbus-jose-jwt:6.0.2'
|
||||
dependency 'com.nimbusds:oauth2-oidc-sdk:6.0'
|
||||
dependency 'com.squareup.okhttp3:okhttp:3.9.0'
|
||||
dependency 'com.squareup.okhttp3:okhttp:3.12.1'
|
||||
dependency 'com.squareup.okio:okio:1.13.0'
|
||||
dependency 'com.sun.xml.bind:jaxb-core:2.3.0.1'
|
||||
dependency 'com.sun.xml.bind:jaxb-impl:2.3.0.1'
|
||||
dependency 'com.unboundid:unboundid-ldapsdk:4.0.8'
|
||||
dependency 'com.unboundid:unboundid-ldapsdk:4.0.9'
|
||||
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'
|
||||
@@ -81,12 +81,12 @@ dependencyManagement {
|
||||
dependency 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'
|
||||
dependency 'junit:junit:4.12'
|
||||
dependency 'ldapsdk:ldapsdk:4.1'
|
||||
dependency 'net.bytebuddy:byte-buddy-agent:1.7.9'
|
||||
dependency 'net.bytebuddy:byte-buddy:1.7.9'
|
||||
dependency 'net.bytebuddy:byte-buddy-agent:1.8.3'
|
||||
dependency 'net.bytebuddy:byte-buddy:1.8.3'
|
||||
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.5'
|
||||
dependency 'net.sf.ehcache:ehcache:2.10.6'
|
||||
dependency 'net.sourceforge.htmlunit:htmlunit:2.33'
|
||||
dependency 'net.sourceforge.htmlunit:neko-htmlunit:2.32'
|
||||
dependency 'net.sourceforge.nekohtml:nekohtml:1.9.22'
|
||||
@@ -127,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.6'
|
||||
dependency 'org.apache.httpcomponents:httpclient:4.5.7'
|
||||
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'
|
||||
@@ -140,12 +140,12 @@ dependencyManagement {
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-logging-log4j:8.0.44'
|
||||
dependency 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.23'
|
||||
dependency 'org.apache.tomcat:tomcat-annotations-api:8.5.23'
|
||||
dependency 'org.aspectj:aspectjrt:1.9.1'
|
||||
dependency 'org.aspectj:aspectjtools:1.9.1'
|
||||
dependency 'org.aspectj:aspectjweaver:1.9.1'
|
||||
dependency 'org.aspectj:aspectjrt:1.9.2'
|
||||
dependency 'org.aspectj:aspectjtools:1.9.2'
|
||||
dependency 'org.aspectj:aspectjweaver:1.9.2'
|
||||
dependency 'org.assertj:assertj-core:3.11.1'
|
||||
dependency 'org.attoparser:attoparser:2.0.4.RELEASE'
|
||||
dependency 'org.bouncycastle:bcpkix-jdk15on:1.60'
|
||||
dependency 'org.bouncycastle:bcpkix-jdk15on:1.61'
|
||||
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'
|
||||
@@ -174,15 +174,15 @@ dependencyManagement {
|
||||
dependency 'org.hibernate:hibernate-entitymanager:5.3.6.Final'
|
||||
dependency 'org.hibernate:hibernate-validator:6.0.13.Final'
|
||||
dependency 'org.hsqldb:hsqldb:2.4.1'
|
||||
dependency 'org.jasig.cas.client:cas-client-core:3.5.0'
|
||||
dependency 'org.jasig.cas.client:cas-client-core:3.5.1'
|
||||
dependency 'org.javassist:javassist:3.22.0-CR2'
|
||||
dependency 'org.jboss.logging:jboss-logging:3.3.1.Final'
|
||||
dependency 'org.jboss.spec.javax.transaction:jboss-transaction-api_1.2_spec:1.0.1.Final'
|
||||
dependency 'org.jboss:jandex:2.0.3.Final'
|
||||
dependency 'org.mockito:mockito-core:2.22.0'
|
||||
dependency 'org.mockito:mockito-core:2.23.4'
|
||||
dependency 'org.objenesis:objenesis:2.6'
|
||||
dependency 'org.openid4java:openid4java-nodeps:0.9.6'
|
||||
dependency 'org.ow2.asm:asm:6.0'
|
||||
dependency 'org.ow2.asm:asm:6.2.1'
|
||||
dependency 'org.reactivestreams:reactive-streams:1.0.1'
|
||||
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.33.0'
|
||||
dependency 'org.seleniumhq.selenium:selenium-java:3.14.0'
|
||||
@@ -195,8 +195,7 @@ dependencyManagement {
|
||||
dependency 'org.slf4j:slf4j-nop:1.7.25'
|
||||
dependency 'org.sonatype.sisu.inject:cglib:2.2.1-v20090111'
|
||||
dependency 'org.springframework.ldap:spring-ldap-core:2.3.2.RELEASE'
|
||||
dependency 'org.thymeleaf:thymeleaf-spring5:3.0.10.RELEASE'
|
||||
dependency 'org.thymeleaf:thymeleaf:3.0.9.RELEASE'
|
||||
dependency 'org.thymeleaf:thymeleaf-spring5:3.0.11.RELEASE'
|
||||
dependency 'org.unbescape:unbescape:1.1.5.RELEASE'
|
||||
dependency 'org.w3c.css:sac:1.3'
|
||||
dependency 'xalan:serializer:2.7.2'
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
+12
-1
@@ -25,6 +25,7 @@ import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Set;
|
||||
@@ -64,7 +65,17 @@ public class WebClientReactiveClientCredentialsTokenResponseClient implements Re
|
||||
.headers(headers(clientRegistration))
|
||||
.body(body)
|
||||
.exchange()
|
||||
.flatMap(response -> response.body(oauth2AccessTokenResponse()))
|
||||
.flatMap(response ->{
|
||||
if (!response.statusCode().is2xxSuccessful()){
|
||||
// extract the contents of this into a method named oauth2AccessTokenResponse but has an argument for the response
|
||||
throw WebClientResponseException.create(response.rawStatusCode(),
|
||||
"Cannot get token, expected 2xx HTTP Status code",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
return response.body(oauth2AccessTokenResponse()); })
|
||||
.map(response -> {
|
||||
if (response.getAccessToken().getScopes().isEmpty()) {
|
||||
response = OAuth2AccessTokenResponse.withResponse(response)
|
||||
|
||||
+12
-10
@@ -18,16 +18,13 @@ package org.springframework.security.oauth2.client.registration;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collector;
|
||||
|
||||
import static java.util.stream.Collectors.collectingAndThen;
|
||||
import static java.util.stream.Collectors.toConcurrentMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A {@link ClientRegistrationRepository} that stores {@link ClientRegistration}(s) in-memory.
|
||||
@@ -39,6 +36,7 @@ import static java.util.stream.Collectors.toConcurrentMap;
|
||||
* @see ClientRegistration
|
||||
*/
|
||||
public final class InMemoryClientRegistrationRepository implements ClientRegistrationRepository, Iterable<ClientRegistration> {
|
||||
|
||||
private final Map<String, ClientRegistration> registrations;
|
||||
|
||||
/**
|
||||
@@ -47,7 +45,8 @@ public final class InMemoryClientRegistrationRepository implements ClientRegistr
|
||||
* @param registrations the client registration(s)
|
||||
*/
|
||||
public InMemoryClientRegistrationRepository(ClientRegistration... registrations) {
|
||||
this(Arrays.asList(registrations));
|
||||
Assert.notEmpty(registrations, "registrations cannot be empty");
|
||||
this.registrations = createClientRegistrationIdToClientRegistration(Arrays.asList(registrations));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,10 +56,13 @@ public final class InMemoryClientRegistrationRepository implements ClientRegistr
|
||||
*/
|
||||
public InMemoryClientRegistrationRepository(List<ClientRegistration> registrations) {
|
||||
Assert.notEmpty(registrations, "registrations cannot be empty");
|
||||
Collector<ClientRegistration, ?, ConcurrentMap<String, ClientRegistration>> collector =
|
||||
toConcurrentMap(ClientRegistration::getRegistrationId, Function.identity());
|
||||
this.registrations = registrations.stream()
|
||||
.collect(collectingAndThen(collector, Collections::unmodifiableMap));
|
||||
this.registrations = createClientRegistrationIdToClientRegistration(registrations);
|
||||
}
|
||||
|
||||
private static Map<String, ClientRegistration> createClientRegistrationIdToClientRegistration(Collection<ClientRegistration> registrations) {
|
||||
return Collections.unmodifiableMap(registrations.stream()
|
||||
.peek(registration -> Assert.notNull(registration, "registrations cannot contain null values"))
|
||||
.collect(Collectors.toMap(ClientRegistration::getRegistrationId, Function.identity())));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-19
@@ -17,12 +17,6 @@ package org.springframework.security.oauth2.client.registration;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -37,7 +31,7 @@ import reactor.core.publisher.Mono;
|
||||
public final class InMemoryReactiveClientRegistrationRepository
|
||||
implements ReactiveClientRegistrationRepository, Iterable<ClientRegistration> {
|
||||
|
||||
private final Map<String, ClientRegistration> clientIdToClientRegistration;
|
||||
private final InMemoryClientRegistrationRepository delegate;
|
||||
|
||||
/**
|
||||
* Constructs an {@code InMemoryReactiveClientRegistrationRepository} using the provided parameters.
|
||||
@@ -45,12 +39,7 @@ public final class InMemoryReactiveClientRegistrationRepository
|
||||
* @param registrations the client registration(s)
|
||||
*/
|
||||
public InMemoryReactiveClientRegistrationRepository(ClientRegistration... registrations) {
|
||||
Assert.notEmpty(registrations, "registrations cannot be empty");
|
||||
this.clientIdToClientRegistration = new ConcurrentReferenceHashMap<>();
|
||||
for (ClientRegistration registration : registrations) {
|
||||
Assert.notNull(registration, "registrations cannot contain null values");
|
||||
this.clientIdToClientRegistration.put(registration.getRegistrationId(), registration);
|
||||
}
|
||||
this.delegate = new InMemoryClientRegistrationRepository(registrations);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,15 +48,12 @@ public final class InMemoryReactiveClientRegistrationRepository
|
||||
* @param registrations the client registration(s)
|
||||
*/
|
||||
public InMemoryReactiveClientRegistrationRepository(List<ClientRegistration> registrations) {
|
||||
Assert.notEmpty(registrations, "registrations cannot be null or empty");
|
||||
this.clientIdToClientRegistration = registrations.stream()
|
||||
.collect(Collectors.toConcurrentMap(ClientRegistration::getRegistrationId, Function.identity()));
|
||||
this.delegate = new InMemoryClientRegistrationRepository(registrations);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<ClientRegistration> findByRegistrationId(String registrationId) {
|
||||
return Mono.justOrEmpty(this.clientIdToClientRegistration.get(registrationId));
|
||||
return Mono.justOrEmpty(this.delegate.findByRegistrationId(registrationId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,6 +63,6 @@ public final class InMemoryReactiveClientRegistrationRepository
|
||||
*/
|
||||
@Override
|
||||
public Iterator<ClientRegistration> iterator() {
|
||||
return this.clientIdToClientRegistration.values().iterator();
|
||||
return delegate.iterator();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ public class OAuth2UserRequestEntityConverter implements Converter<OAuth2UserReq
|
||||
httpMethod = HttpMethod.POST;
|
||||
}
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri())
|
||||
.build()
|
||||
.toUri();
|
||||
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -45,6 +45,7 @@ import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
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;
|
||||
@@ -267,7 +268,11 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements
|
||||
.build();
|
||||
return next.exchange(refreshRequest)
|
||||
.flatMap(refreshResponse -> refreshResponse.body(oauth2AccessTokenResponse()))
|
||||
.map(accessTokenResponse -> new OAuth2AuthorizedClient(authorizedClient.getClientRegistration(), authorizedClient.getPrincipalName(), accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken()))
|
||||
.map(accessTokenResponse -> {
|
||||
OAuth2RefreshToken refreshToken = Optional.ofNullable(accessTokenResponse.getRefreshToken())
|
||||
.orElse(authorizedClient.getRefreshToken());
|
||||
return new OAuth2AuthorizedClient(authorizedClient.getClientRegistration(), authorizedClient.getPrincipalName(), accessTokenResponse.getAccessToken(), refreshToken);
|
||||
})
|
||||
.flatMap(result -> this.authorizedClientRepository.saveAuthorizedClient(result, authentication, exchange)
|
||||
.thenReturn(result));
|
||||
}
|
||||
|
||||
+119
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.security.oauth2.client.web.reactive.function.client;
|
||||
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -44,8 +47,12 @@ 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.CoreSubscriber;
|
||||
import reactor.core.publisher.Hooks;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Operators;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -98,7 +105,9 @@ import static org.springframework.security.oauth2.core.web.reactive.function.OAu
|
||||
* @author Rob Winch
|
||||
* @since 5.1
|
||||
*/
|
||||
public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction {
|
||||
public final class ServletOAuth2AuthorizedClientExchangeFilterFunction
|
||||
implements ExchangeFilterFunction, InitializingBean, DisposableBean {
|
||||
|
||||
/**
|
||||
* The request attribute name used to locate the {@link OAuth2AuthorizedClient}.
|
||||
*/
|
||||
@@ -108,6 +117,8 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
|
||||
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 static final String REQUEST_CONTEXT_OPERATOR_KEY = RequestContextSubscriber.class.getName();
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
private Duration accessTokenExpiresSkew = Duration.ofMinutes(1);
|
||||
@@ -123,7 +134,8 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
|
||||
|
||||
private String defaultClientRegistrationId;
|
||||
|
||||
public ServletOAuth2AuthorizedClientExchangeFilterFunction() {}
|
||||
public ServletOAuth2AuthorizedClientExchangeFilterFunction() {
|
||||
}
|
||||
|
||||
public ServletOAuth2AuthorizedClientExchangeFilterFunction(
|
||||
ClientRegistrationRepository clientRegistrationRepository,
|
||||
@@ -132,6 +144,16 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
|
||||
this.authorizedClientRepository = authorizedClientRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Hooks.onLastOperator(REQUEST_CONTEXT_OPERATOR_KEY, Operators.lift((s, sub) -> createRequestContextSubscriber(sub)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
Hooks.resetOnLastOperator(REQUEST_CONTEXT_OPERATOR_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2AccessTokenResponseClient} to be used for getting an {@link OAuth2AuthorizedClient} for
|
||||
* client_credentials grant.
|
||||
@@ -266,15 +288,36 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
|
||||
|
||||
@Override
|
||||
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
|
||||
Optional<OAuth2AuthorizedClient> attribute = request.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME)
|
||||
.map(OAuth2AuthorizedClient.class::cast);
|
||||
return Mono.justOrEmpty(attribute)
|
||||
.flatMap(authorizedClient -> authorizedClient(request, next, authorizedClient))
|
||||
return Mono.just(request)
|
||||
.filter(req -> req.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME).isPresent())
|
||||
.switchIfEmpty(mergeRequestAttributesFromContext(request))
|
||||
.filter(req -> req.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME).isPresent())
|
||||
.flatMap(req -> authorizedClient(req, next, getOAuth2AuthorizedClient(req.attributes())))
|
||||
.map(authorizedClient -> bearer(request, authorizedClient))
|
||||
.flatMap(next::exchange)
|
||||
.switchIfEmpty(next.exchange(request));
|
||||
}
|
||||
|
||||
private Mono<ClientRequest> mergeRequestAttributesFromContext(ClientRequest request) {
|
||||
return Mono.just(ClientRequest.from(request))
|
||||
.flatMap(builder -> Mono.subscriberContext()
|
||||
.map(ctx -> builder.attributes(attrs -> populateRequestAttributes(attrs, ctx))))
|
||||
.map(ClientRequest.Builder::build);
|
||||
}
|
||||
|
||||
private void populateRequestAttributes(Map<String, Object> attrs, Context ctx) {
|
||||
if (ctx.hasKey(HTTP_SERVLET_REQUEST_ATTR_NAME)) {
|
||||
attrs.putIfAbsent(HTTP_SERVLET_REQUEST_ATTR_NAME, ctx.get(HTTP_SERVLET_REQUEST_ATTR_NAME));
|
||||
}
|
||||
if (ctx.hasKey(HTTP_SERVLET_RESPONSE_ATTR_NAME)) {
|
||||
attrs.putIfAbsent(HTTP_SERVLET_RESPONSE_ATTR_NAME, ctx.get(HTTP_SERVLET_RESPONSE_ATTR_NAME));
|
||||
}
|
||||
if (ctx.hasKey(AUTHENTICATION_ATTR_NAME)) {
|
||||
attrs.putIfAbsent(AUTHENTICATION_ATTR_NAME, ctx.get(AUTHENTICATION_ATTR_NAME));
|
||||
}
|
||||
populateDefaultOAuth2AuthorizedClient(attrs);
|
||||
}
|
||||
|
||||
private void populateDefaultRequestResponse(Map<String, Object> attrs) {
|
||||
if (attrs.containsKey(HTTP_SERVLET_REQUEST_ATTR_NAME) && attrs.containsKey(
|
||||
HTTP_SERVLET_RESPONSE_ATTR_NAME)) {
|
||||
@@ -385,7 +428,11 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
|
||||
.build();
|
||||
return next.exchange(refreshRequest)
|
||||
.flatMap(response -> response.body(oauth2AccessTokenResponse()))
|
||||
.map(accessTokenResponse -> new OAuth2AuthorizedClient(authorizedClient.getClientRegistration(), authorizedClient.getPrincipalName(), accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken()))
|
||||
.map(accessTokenResponse -> {
|
||||
OAuth2RefreshToken refreshToken = Optional.ofNullable(accessTokenResponse.getRefreshToken())
|
||||
.orElse(authorizedClient.getRefreshToken());
|
||||
return new OAuth2AuthorizedClient(authorizedClient.getClientRegistration(), authorizedClient.getPrincipalName(), accessTokenResponse.getAccessToken(), refreshToken);
|
||||
})
|
||||
.map(result -> {
|
||||
Authentication principal = (Authentication) request.attribute(
|
||||
AUTHENTICATION_ATTR_NAME).orElse(new PrincipalNameAuthentication(authorizedClient.getPrincipalName()));
|
||||
@@ -421,6 +468,19 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
|
||||
.build();
|
||||
}
|
||||
|
||||
private <T> CoreSubscriber<T> createRequestContextSubscriber(CoreSubscriber<T> delegate) {
|
||||
HttpServletRequest request = null;
|
||||
HttpServletResponse response = null;
|
||||
ServletRequestAttributes requestAttributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (requestAttributes != null) {
|
||||
request = requestAttributes.getRequest();
|
||||
response = requestAttributes.getResponse();
|
||||
}
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return new RequestContextSubscriber<>(delegate, request, response, authentication);
|
||||
}
|
||||
|
||||
private static BodyInserters.FormInserter<String> refreshTokenBody(String refreshToken) {
|
||||
return BodyInserters
|
||||
.fromFormData("grant_type", AuthorizationGrantType.REFRESH_TOKEN.getValue())
|
||||
@@ -494,4 +554,55 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
|
||||
return new UnsupportedOperationException("Not Supported");
|
||||
}
|
||||
}
|
||||
|
||||
private static class RequestContextSubscriber<T> implements CoreSubscriber<T> {
|
||||
private static final String CONTEXT_DEFAULTED_ATTR_NAME = RequestContextSubscriber.class.getName().concat(".CONTEXT_DEFAULTED_ATTR_NAME");
|
||||
private final CoreSubscriber<T> delegate;
|
||||
private final HttpServletRequest request;
|
||||
private final HttpServletResponse response;
|
||||
private final Authentication authentication;
|
||||
|
||||
private RequestContextSubscriber(CoreSubscriber<T> delegate,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Authentication authentication) {
|
||||
this.delegate = delegate;
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
this.authentication = authentication;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context currentContext() {
|
||||
Context context = this.delegate.currentContext();
|
||||
if (context.hasKey(CONTEXT_DEFAULTED_ATTR_NAME)) {
|
||||
return context;
|
||||
}
|
||||
return Context.of(
|
||||
CONTEXT_DEFAULTED_ATTR_NAME, Boolean.TRUE,
|
||||
HTTP_SERVLET_REQUEST_ATTR_NAME, this.request,
|
||||
HTTP_SERVLET_RESPONSE_ATTR_NAME, this.response,
|
||||
AUTHENTICATION_ATTR_NAME, this.authentication);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
this.delegate.onSubscribe(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(T t) {
|
||||
this.delegate.onNext(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
this.delegate.onError(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
this.delegate.onComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -28,6 +28,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
@@ -116,6 +117,23 @@ public class WebClientReactiveClientCredentialsTokenResponseClientTests {
|
||||
assertThat(response.getAccessToken().getScopes()).isEqualTo(registration.getScopes());
|
||||
}
|
||||
|
||||
@Test(expected = WebClientResponseException.class)
|
||||
// gh-6089
|
||||
public void getTokenResponseWhenInvalidResponse() throws WebClientResponseException {
|
||||
ClientRegistration registration = this.clientRegistration.build();
|
||||
enqueueUnexpectedResponse();
|
||||
|
||||
OAuth2ClientCredentialsGrantRequest request = new OAuth2ClientCredentialsGrantRequest(registration);
|
||||
|
||||
OAuth2AccessTokenResponse response = this.client.getTokenResponse(request).block();
|
||||
}
|
||||
|
||||
private void enqueueUnexpectedResponse(){
|
||||
MockResponse response = new MockResponse()
|
||||
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.setResponseCode(301);
|
||||
this.server.enqueue(response);
|
||||
}
|
||||
|
||||
private void enqueueJson(String body) {
|
||||
MockResponse response = new MockResponse()
|
||||
|
||||
+3
-3
@@ -316,7 +316,7 @@ public class OidcUserServiceTests {
|
||||
|
||||
this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken));
|
||||
assertThat(this.server.takeRequest(1, TimeUnit.SECONDS).getHeader(HttpHeaders.ACCEPT))
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
|
||||
// gh-5500
|
||||
@@ -341,7 +341,7 @@ public class OidcUserServiceTests {
|
||||
this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken));
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name());
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue());
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ public class OidcUserServiceTests {
|
||||
this.userService.loadUser(new OidcUserRequest(this.clientRegistration, this.accessToken, this.idToken));
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name());
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
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());
|
||||
}
|
||||
|
||||
+6
@@ -35,6 +35,12 @@ public class InMemoryClientRegistrationRepositoryTests {
|
||||
|
||||
private InMemoryClientRegistrationRepository clients = new InMemoryClientRegistrationRepository(this.registration);
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorVarArgsListClientRegistrationWhenNullThenIllegalArgumentException() {
|
||||
ClientRegistration nullRegistration = null;
|
||||
new InMemoryClientRegistrationRepository(nullRegistration);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorListClientRegistrationWhenNullThenIllegalArgumentException() {
|
||||
List<ClientRegistration> registrations = null;
|
||||
|
||||
+9
-2
@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.client.registration;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
@@ -61,10 +62,16 @@ public class InMemoryReactiveClientRegistrationRepositoryTests {
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenClientRegistrationListHasNullThenIllegalArgumentException() {
|
||||
List<ClientRegistration> registrations = Arrays.asList(null, registration);
|
||||
assertThatThrownBy(() -> new InMemoryReactiveClientRegistrationRepository(registrations))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenClientRegistrationIsNullThenIllegalArgumentException() {
|
||||
ClientRegistration registration = null;
|
||||
assertThatThrownBy(() -> new InMemoryReactiveClientRegistrationRepository(registration))
|
||||
assertThatThrownBy(() -> new InMemoryReactiveClientRegistrationRepository(registration, null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -277,7 +277,7 @@ public class DefaultOAuth2UserServiceTests {
|
||||
|
||||
this.userService.loadUser(new OAuth2UserRequest(this.clientRegistration, this.accessToken));
|
||||
assertThat(this.server.takeRequest(1, TimeUnit.SECONDS).getHeader(HttpHeaders.ACCEPT))
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
|
||||
// gh-5500
|
||||
@@ -303,7 +303,7 @@ public class DefaultOAuth2UserServiceTests {
|
||||
this.userService.loadUser(new OAuth2UserRequest(this.clientRegistration, this.accessToken));
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo(HttpMethod.GET.name());
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertThat(request.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer " + this.accessToken.getTokenValue());
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ public class DefaultOAuth2UserServiceTests {
|
||||
this.userService.loadUser(new OAuth2UserRequest(this.clientRegistration, this.accessToken));
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo(HttpMethod.POST.name());
|
||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo(MediaType.APPLICATION_JSON_UTF8_VALUE);
|
||||
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());
|
||||
}
|
||||
|
||||
+2
-2
@@ -56,7 +56,7 @@ public class OAuth2UserRequestEntityConverterTests {
|
||||
clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri());
|
||||
|
||||
HttpHeaders headers = requestEntity.getHeaders();
|
||||
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
|
||||
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON);
|
||||
assertThat(headers.getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo(
|
||||
"Bearer " + userRequest.getAccessToken().getTokenValue());
|
||||
}
|
||||
@@ -77,7 +77,7 @@ public class OAuth2UserRequestEntityConverterTests {
|
||||
clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri());
|
||||
|
||||
HttpHeaders headers = requestEntity.getHeaders();
|
||||
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON_UTF8);
|
||||
assertThat(headers.getAccept()).contains(MediaType.APPLICATION_JSON);
|
||||
assertThat(headers.getContentType()).isEqualTo(
|
||||
MediaType.valueOf(APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
|
||||
|
||||
|
||||
+35
-7
@@ -107,7 +107,11 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
||||
assertThat(authorizationRequest.getState()).isNotNull();
|
||||
assertThat(authorizationRequest.getAdditionalParameters())
|
||||
.containsExactly(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id&scope=read%3Auser&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-id");
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/login/oauth2/code/registration-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -164,7 +168,11 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
||||
request.setServletPath(requestUri);
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id&scope=read%3Auser&state=.{15,}&redirect_uri=http%3A%2F%2Fexample.com%2Flogin%2Foauth2%2Fcode%2Fregistration-id");
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://example.com/login/oauth2/code/registration-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -178,7 +186,11 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
||||
request.setServletPath(requestUri);
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id&scope=read%3Auser&state=.{15,}&redirect_uri=https%3A%2F%2Fexample.com%2Flogin%2Foauth2%2Fcode%2Fregistration-id");
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=https://example.com/login/oauth2/code/registration-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -189,7 +201,11 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
||||
request.setServletPath(requestUri);
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request, clientRegistration.getRegistrationId());
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id&scope=read%3Auser&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fcode%2Fregistration-id");
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/authorize/oauth2/code/registration-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -200,7 +216,11 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
||||
request.setServletPath(requestUri);
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id-2&scope=read%3Auser&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-id-2");
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id-2&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/login/oauth2/code/registration-id-2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -212,7 +232,11 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
||||
request.setServletPath(requestUri);
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id&scope=read%3Auser&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fcode%2Fregistration-id");
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/authorize/oauth2/code/registration-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -224,6 +248,10 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
|
||||
request.setServletPath(requestUri);
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request);
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id-2&scope=read%3Auser&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-id-2");
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id-2&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/login/oauth2/code/registration-id-2");
|
||||
}
|
||||
}
|
||||
|
||||
+26
-6
@@ -151,7 +151,10 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
|
||||
|
||||
verifyZeroInteractions(filterChain);
|
||||
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id&scope=read%3Auser&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-id");
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/login/oauth2/code/registration-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -187,7 +190,10 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
|
||||
|
||||
verifyZeroInteractions(filterChain);
|
||||
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?response_type=token&client_id=client-id&scope=read%3Auser&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fimplicit%2Fregistration-3");
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=token&client_id=client-id&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/authorize/oauth2/implicit/registration-3");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -225,7 +231,10 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
|
||||
|
||||
verifyZeroInteractions(filterChain);
|
||||
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id&scope=read%3Auser&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-id");
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/login/oauth2/code/registration-id");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -243,7 +252,10 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id&scope=read%3Auser&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fcode%2Fregistration-id");
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/authorize/oauth2/code/registration-id");
|
||||
verify(this.requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@@ -298,7 +310,11 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
|
||||
|
||||
verifyZeroInteractions(filterChain);
|
||||
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id&scope=read%3Auser&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-id&idp=https%3A%2F%2Fother.provider.com");
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/login/oauth2/code/registration-id&" +
|
||||
"idp=https://other.provider.com");
|
||||
}
|
||||
|
||||
// gh-4911, gh-5244
|
||||
@@ -339,6 +355,10 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
|
||||
|
||||
verifyZeroInteractions(filterChain);
|
||||
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id&scope=read%3Auser&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fregistration-id&login_hint=user@provider\\.com");
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=read:user&state=.{15,}&" +
|
||||
"redirect_uri=http://localhost/login/oauth2/code/registration-id&" +
|
||||
"login_hint=user@provider\\.com");
|
||||
}
|
||||
}
|
||||
|
||||
+62
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,6 +19,8 @@ package org.springframework.security.oauth2.client.web.reactive.function.client;
|
||||
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;
|
||||
@@ -89,6 +91,9 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||
@Mock
|
||||
private ServerWebExchange serverWebExchange;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<OAuth2AuthorizedClient> authorizedClientCaptor;
|
||||
|
||||
private ServerOAuth2AuthorizedClientExchangeFilterFunction function;
|
||||
|
||||
private MockExchangeFunction exchange = new MockExchangeFunction();
|
||||
@@ -173,7 +178,62 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||
.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication))
|
||||
.block();
|
||||
|
||||
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(authentication), any());
|
||||
verify(this.authorizedClientRepository).saveAuthorizedClient(this.authorizedClientCaptor.capture(), eq(authentication), any());
|
||||
|
||||
OAuth2AuthorizedClient newAuthorizedClient = authorizedClientCaptor.getValue();
|
||||
assertThat(newAuthorizedClient.getAccessToken()).isEqualTo(response.getAccessToken());
|
||||
assertThat(newAuthorizedClient.getRefreshToken()).isEqualTo(response.getRefreshToken());
|
||||
|
||||
List<ClientRequest> requests = this.exchange.getRequests();
|
||||
assertThat(requests).hasSize(2);
|
||||
|
||||
ClientRequest request0 = requests.get(0);
|
||||
assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50LWlkOmNsaWVudC1zZWNyZXQ=");
|
||||
assertThat(request0.url().toASCIIString()).isEqualTo("https://example.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 filterWhenRefreshRequiredThenRefreshAndResponseDoesNotContainRefreshToken() {
|
||||
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any())).thenReturn(Mono.empty());
|
||||
OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1")
|
||||
.tokenType(OAuth2AccessToken.TokenType.BEARER)
|
||||
.expiresIn(3600)
|
||||
// .refreshToken(xxx) // No refreshToken in response
|
||||
.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));
|
||||
|
||||
this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(),
|
||||
this.accessToken.getTokenValue(),
|
||||
issuedAt,
|
||||
accessTokenExpiresAt);
|
||||
|
||||
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt);
|
||||
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration,
|
||||
"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.authorizedClientRepository).saveAuthorizedClient(this.authorizedClientCaptor.capture(), eq(authentication), any());
|
||||
|
||||
OAuth2AuthorizedClient newAuthorizedClient = authorizedClientCaptor.getValue();
|
||||
assertThat(newAuthorizedClient.getAccessToken()).isEqualTo(response.getAccessToken());
|
||||
assertThat(newAuthorizedClient.getRefreshToken()).isEqualTo(authorizedClient.getRefreshToken());
|
||||
|
||||
List<ClientRequest> requests = this.exchange.getRequests();
|
||||
assertThat(requests).hasSize(2);
|
||||
|
||||
+176
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -74,13 +74,11 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
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.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.mockito.Mockito.*;
|
||||
import static org.springframework.http.HttpMethod.GET;
|
||||
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.*;
|
||||
|
||||
@@ -100,6 +98,8 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||
private WebClient.RequestHeadersSpec<?> spec;
|
||||
@Captor
|
||||
private ArgumentCaptor<Consumer<Map<String, Object>>> attrs;
|
||||
@Captor
|
||||
private ArgumentCaptor<OAuth2AuthorizedClient> authorizedClientCaptor;
|
||||
|
||||
/**
|
||||
* Used for get the attributes from defaultRequest.
|
||||
@@ -405,7 +405,61 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||
|
||||
this.function.filter(request, this.exchange).block();
|
||||
|
||||
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), eq(this.authentication), any(), any());
|
||||
verify(this.authorizedClientRepository).saveAuthorizedClient(this.authorizedClientCaptor.capture(), eq(this.authentication), any(), any());
|
||||
|
||||
OAuth2AuthorizedClient newAuthorizedClient = authorizedClientCaptor.getValue();
|
||||
assertThat(newAuthorizedClient.getAccessToken()).isEqualTo(response.getAccessToken());
|
||||
assertThat(newAuthorizedClient.getRefreshToken()).isEqualTo(response.getRefreshToken());
|
||||
|
||||
List<ClientRequest> requests = this.exchange.getRequests();
|
||||
assertThat(requests).hasSize(2);
|
||||
|
||||
ClientRequest request0 = requests.get(0);
|
||||
assertThat(request0.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Basic Y2xpZW50LWlkOmNsaWVudC1zZWNyZXQ=");
|
||||
assertThat(request0.url().toASCIIString()).isEqualTo("https://example.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 filterWhenRefreshRequiredThenRefreshAndResponseDoesNotContainRefreshToken() {
|
||||
OAuth2AccessTokenResponse response = OAuth2AccessTokenResponse.withToken("token-1")
|
||||
.tokenType(OAuth2AccessToken.TokenType.BEARER)
|
||||
.expiresIn(3600)
|
||||
// .refreshToken(xxx) // No refreshToken in response
|
||||
.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));
|
||||
|
||||
this.accessToken = new OAuth2AccessToken(this.accessToken.getTokenType(),
|
||||
this.accessToken.getTokenValue(),
|
||||
issuedAt,
|
||||
accessTokenExpiresAt);
|
||||
this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(this.clientRegistrationRepository,
|
||||
this.authorizedClientRepository);
|
||||
|
||||
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", issuedAt);
|
||||
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.registration,
|
||||
"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(this.authorizedClientCaptor.capture(), eq(this.authentication), any(), any());
|
||||
|
||||
OAuth2AuthorizedClient newAuthorizedClient = authorizedClientCaptor.getValue();
|
||||
assertThat(newAuthorizedClient.getAccessToken()).isEqualTo(response.getAccessToken());
|
||||
assertThat(newAuthorizedClient.getRefreshToken()).isEqualTo(refreshToken);
|
||||
|
||||
List<ClientRequest> requests = this.exchange.getRequests();
|
||||
assertThat(requests).hasSize(2);
|
||||
@@ -516,6 +570,121 @@ public class ServletOAuth2AuthorizedClientExchangeFilterFunctionTests {
|
||||
assertThat(getBody(request0)).isEmpty();
|
||||
}
|
||||
|
||||
// gh-6483
|
||||
@Test
|
||||
public void filterWhenChainedThenDefaultsStillAvailable() throws Exception {
|
||||
this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
|
||||
this.clientRegistrationRepository, this.authorizedClientRepository);
|
||||
this.function.afterPropertiesSet(); // Hooks.onLastOperator() initialized
|
||||
this.function.setDefaultOAuth2AuthorizedClient(true);
|
||||
|
||||
MockHttpServletRequest servletRequest = new MockHttpServletRequest();
|
||||
MockHttpServletResponse servletResponse = new MockHttpServletResponse();
|
||||
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(servletRequest, servletResponse));
|
||||
|
||||
OAuth2User user = mock(OAuth2User.class);
|
||||
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
|
||||
OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(
|
||||
user, authorities, this.registration.getRegistrationId());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
|
||||
this.registration, "principalName", this.accessToken);
|
||||
when(this.authorizedClientRepository.loadAuthorizedClient(eq(authentication.getAuthorizedClientRegistrationId()),
|
||||
eq(authentication), eq(servletRequest))).thenReturn(authorizedClient);
|
||||
|
||||
// Default request attributes set
|
||||
final ClientRequest request1 = ClientRequest.create(GET, URI.create("https://example1.com"))
|
||||
.attributes(attrs -> attrs.putAll(getDefaultRequestAttributes())).build();
|
||||
|
||||
// Default request attributes NOT set
|
||||
final ClientRequest request2 = ClientRequest.create(GET, URI.create("https://example2.com")).build();
|
||||
|
||||
this.function.filter(request1, this.exchange)
|
||||
.flatMap(response -> this.function.filter(request2, this.exchange))
|
||||
.block();
|
||||
|
||||
this.function.destroy(); // Hooks.onLastOperator() released
|
||||
|
||||
List<ClientRequest> requests = this.exchange.getRequests();
|
||||
assertThat(requests).hasSize(2);
|
||||
|
||||
ClientRequest request = requests.get(0);
|
||||
assertThat(request.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0");
|
||||
assertThat(request.url().toASCIIString()).isEqualTo("https://example1.com");
|
||||
assertThat(request.method()).isEqualTo(HttpMethod.GET);
|
||||
assertThat(getBody(request)).isEmpty();
|
||||
|
||||
request = requests.get(1);
|
||||
assertThat(request.headers().getFirst(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer token-0");
|
||||
assertThat(request.url().toASCIIString()).isEqualTo("https://example2.com");
|
||||
assertThat(request.method()).isEqualTo(HttpMethod.GET);
|
||||
assertThat(getBody(request)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterWhenRequestAttributesNotSetAndHooksNotInitThenDefaultsNotAvailable() throws Exception {
|
||||
this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
|
||||
this.clientRegistrationRepository, this.authorizedClientRepository);
|
||||
// this.function.afterPropertiesSet(); // Hooks.onLastOperator() NOT initialized
|
||||
this.function.setDefaultOAuth2AuthorizedClient(true);
|
||||
|
||||
MockHttpServletRequest servletRequest = new MockHttpServletRequest();
|
||||
MockHttpServletResponse servletResponse = new MockHttpServletResponse();
|
||||
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(servletRequest, servletResponse));
|
||||
|
||||
OAuth2User user = mock(OAuth2User.class);
|
||||
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
|
||||
OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(
|
||||
user, authorities, this.registration.getRegistrationId());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")).build();
|
||||
|
||||
this.function.filter(request, this.exchange).block();
|
||||
|
||||
List<ClientRequest> requests = this.exchange.getRequests();
|
||||
assertThat(requests).hasSize(1);
|
||||
|
||||
request = requests.get(0);
|
||||
assertThat(request.headers().getFirst(HttpHeaders.AUTHORIZATION)).isNull();
|
||||
assertThat(request.url().toASCIIString()).isEqualTo("https://example.com");
|
||||
assertThat(request.method()).isEqualTo(HttpMethod.GET);
|
||||
assertThat(getBody(request)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterWhenRequestAttributesNotSetAndHooksInitHooksResetThenDefaultsNotAvailable() throws Exception {
|
||||
this.function = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
|
||||
this.clientRegistrationRepository, this.authorizedClientRepository);
|
||||
this.function.afterPropertiesSet(); // Hooks.onLastOperator() initialized
|
||||
this.function.destroy(); // Hooks.onLastOperator() released
|
||||
this.function.setDefaultOAuth2AuthorizedClient(true);
|
||||
|
||||
MockHttpServletRequest servletRequest = new MockHttpServletRequest();
|
||||
MockHttpServletResponse servletResponse = new MockHttpServletResponse();
|
||||
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(servletRequest, servletResponse));
|
||||
|
||||
OAuth2User user = mock(OAuth2User.class);
|
||||
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
|
||||
OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(
|
||||
user, authorities, this.registration.getRegistrationId());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
ClientRequest request = ClientRequest.create(GET, URI.create("https://example.com")).build();
|
||||
|
||||
this.function.filter(request, this.exchange).block();
|
||||
|
||||
List<ClientRequest> requests = this.exchange.getRequests();
|
||||
assertThat(requests).hasSize(1);
|
||||
|
||||
request = requests.get(0);
|
||||
assertThat(request.headers().getFirst(HttpHeaders.AUTHORIZATION)).isNull();
|
||||
assertThat(request.url().toASCIIString()).isEqualTo("https://example.com");
|
||||
assertThat(request.method()).isEqualTo(HttpMethod.GET);
|
||||
assertThat(getBody(request)).isEmpty();
|
||||
}
|
||||
|
||||
private static String getBody(ClientRequest request) {
|
||||
final List<HttpMessageWriter<?>> messageWriters = new ArrayList<>();
|
||||
messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
|
||||
|
||||
+4
-1
@@ -77,7 +77,10 @@ public class DefaultServerOAuth2AuthorizationRequestResolverTests {
|
||||
|
||||
OAuth2AuthorizationRequest request = resolve("/oauth2/authorization/not-found-id");
|
||||
|
||||
assertThat(request.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?response_type=code&client_id=client-id&scope=read%3Auser&state=.*?&redirect_uri=%2Flogin%2Foauth2%2Fcode%2Fregistration-id");
|
||||
assertThat(request.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=read:user&state=.*?&" +
|
||||
"redirect_uri=/login/oauth2/code/registration-id");
|
||||
}
|
||||
|
||||
private OAuth2AuthorizationRequest resolve(String path) {
|
||||
|
||||
@@ -3,6 +3,7 @@ apply plugin: 'io.spring.convention.spring-module'
|
||||
dependencies {
|
||||
compile project(':spring-security-core')
|
||||
compile springCoreDependency
|
||||
compile 'org.springframework:spring-web'
|
||||
|
||||
optional 'com.fasterxml.jackson.core:jackson-databind'
|
||||
optional 'com.nimbusds:oauth2-oidc-sdk'
|
||||
|
||||
+22
-25
@@ -15,24 +15,25 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.endpoint;
|
||||
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
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.nio.charset.StandardCharsets;
|
||||
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;
|
||||
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
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.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* A representation of an OAuth 2.0 Authorization Request
|
||||
* for the authorization code grant type or implicit grant type.
|
||||
@@ -336,34 +337,30 @@ public final class OAuth2AuthorizationRequest implements Serializable {
|
||||
}
|
||||
|
||||
private String buildAuthorizationRequestUri() {
|
||||
Map<String, String> parameters = new LinkedHashMap<>();
|
||||
parameters.put(OAuth2ParameterNames.RESPONSE_TYPE, this.responseType.getValue());
|
||||
parameters.put(OAuth2ParameterNames.CLIENT_ID, this.clientId);
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, this.responseType.getValue());
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ID, this.clientId);
|
||||
if (!CollectionUtils.isEmpty(this.scopes)) {
|
||||
parameters.put(OAuth2ParameterNames.SCOPE,
|
||||
parameters.set(OAuth2ParameterNames.SCOPE,
|
||||
StringUtils.collectionToDelimitedString(this.scopes, " "));
|
||||
}
|
||||
if (this.state != null) {
|
||||
parameters.put(OAuth2ParameterNames.STATE, this.state);
|
||||
parameters.set(OAuth2ParameterNames.STATE, this.state);
|
||||
}
|
||||
if (this.redirectUri != null) {
|
||||
parameters.put(OAuth2ParameterNames.REDIRECT_URI, this.redirectUri);
|
||||
parameters.set(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()));
|
||||
.forEach(e -> parameters.set(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);
|
||||
}
|
||||
return UriComponentsBuilder.fromHttpUrl(this.authorizationUri)
|
||||
.queryParams(parameters)
|
||||
.encode(StandardCharsets.UTF_8)
|
||||
.build()
|
||||
.toUriString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -55,15 +55,15 @@ class OAuth2AccessTokenResponseBodyExtractor
|
||||
@Override
|
||||
public Mono<OAuth2AccessTokenResponse> extract(ReactiveHttpInputMessage inputMessage,
|
||||
Context context) {
|
||||
ParameterizedTypeReference<Map<String, String>> type = new ParameterizedTypeReference<Map<String, String>>() {};
|
||||
BodyExtractor<Mono<Map<String, String>>, ReactiveHttpInputMessage> delegate = BodyExtractors.toMono(type);
|
||||
ParameterizedTypeReference<Map<String, Object>> type = new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
BodyExtractor<Mono<Map<String, Object>>, ReactiveHttpInputMessage> delegate = BodyExtractors.toMono(type);
|
||||
return delegate.extract(inputMessage, context)
|
||||
.map(json -> parse(json))
|
||||
.map(OAuth2AccessTokenResponseBodyExtractor::parse)
|
||||
.flatMap(OAuth2AccessTokenResponseBodyExtractor::oauth2AccessTokenResponse)
|
||||
.map(OAuth2AccessTokenResponseBodyExtractor::oauth2AccessTokenResponse);
|
||||
}
|
||||
|
||||
private static TokenResponse parse(Map<String, String> json) {
|
||||
private static TokenResponse parse(Map<String, Object> json) {
|
||||
try {
|
||||
return TokenResponse.parse(new JSONObject(json));
|
||||
}
|
||||
|
||||
+38
-8
@@ -15,16 +15,19 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.endpoint;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2AuthorizationRequest}.
|
||||
@@ -194,7 +197,11 @@ public class OAuth2AuthorizationRequestTests {
|
||||
.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");
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.isEqualTo("https://provider.com/oauth2/authorize?" +
|
||||
"response_type=token&client_id=client-id&" +
|
||||
"scope=scope1%20scope2&state=state&" +
|
||||
"redirect_uri=http://example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -226,7 +233,11 @@ public class OAuth2AuthorizationRequestTests {
|
||||
.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");
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.isEqualTo("https://provider.com/oauth2/authorize?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=scope1%20scope2&state=state&" +
|
||||
"redirect_uri=http://example.com¶m1=value1¶m2=value2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -248,13 +259,17 @@ public class OAuth2AuthorizationRequestTests {
|
||||
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationUri(AUTHORIZATION_URI)
|
||||
.clientId(CLIENT_ID)
|
||||
.redirectUri(REDIRECT_URI)
|
||||
.redirectUri(REDIRECT_URI + "?rparam1=rvalue1&rparam2=rvalue2")
|
||||
.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");
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.isEqualTo("https://provider.com/oauth2/authorize?" +
|
||||
"response_type=code&client_id=client-id&" +
|
||||
"scope=scope1%20scope2&state=state&" +
|
||||
"redirect_uri=http://example.com?rparam1%3Drvalue1%26rparam2%3Drvalue2¶m1=value1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -290,4 +305,19 @@ public class OAuth2AuthorizationRequestTests {
|
||||
assertThat(authorizationRequestCopy.getAdditionalParameters()).isEqualTo(authorizationRequest.getAdditionalParameters());
|
||||
assertThat(authorizationRequestCopy.getAuthorizationRequestUri()).isEqualTo(authorizationRequest.getAuthorizationRequestUri());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAuthorizationUriIncludesQueryParameterThenAuthorizationRequestUrlIncludesIt() {
|
||||
OAuth2AuthorizationRequest authorizationRequest =
|
||||
TestOAuth2AuthorizationRequests.request()
|
||||
.authorizationUri(AUTHORIZATION_URI +
|
||||
"?param1=value1¶m2=value2").build();
|
||||
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri()).isNotNull();
|
||||
assertThat(authorizationRequest.getAuthorizationRequestUri())
|
||||
.isEqualTo("https://provider.com/oauth2/authorize?" +
|
||||
"param1=value1¶m2=value2&" +
|
||||
"response_type=code&client_id=client-id&state=state&" +
|
||||
"redirect_uri=https://example.com/authorize/oauth2/code/registration-id");
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -120,4 +120,33 @@ public class OAuth2BodyExtractorsTests {
|
||||
assertThat(result.getRefreshToken().getTokenValue()).isEqualTo("tGzv3JOkF0XG5Qx2TlKWIA");
|
||||
assertThat(result.getAdditionalParameters()).containsEntry("example_parameter", "example_value");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
// gh-6087
|
||||
public void oauth2AccessTokenResponseWhenMultipleAttributeTypesThenCreated() throws Exception {
|
||||
BodyExtractor<Mono<OAuth2AccessTokenResponse>, 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"
|
||||
+ " \"subjson\":{}, \n"
|
||||
+ " \"list\":[] \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().get("subjson")).isInstanceOfAny(Map.class);
|
||||
assertThat(result.getAdditionalParameters().get("list")).isInstanceOfAny(List.class);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-13
@@ -15,9 +15,6 @@
|
||||
*/
|
||||
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;
|
||||
@@ -37,7 +34,7 @@ public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
|
||||
"This iss claim is not equal to the configured issuer",
|
||||
"https://tools.ietf.org/html/rfc6750#section-3.1");
|
||||
|
||||
private final URL issuer;
|
||||
private final String issuer;
|
||||
|
||||
/**
|
||||
* Constructs a {@link JwtIssuerValidator} using the provided parameters
|
||||
@@ -46,14 +43,7 @@ public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
|
||||
*/
|
||||
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);
|
||||
}
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +53,8 @@ public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
|
||||
public OAuth2TokenValidatorResult validate(Jwt token) {
|
||||
Assert.notNull(token, "token cannot be null");
|
||||
|
||||
if (this.issuer.equals(token.getIssuer())) {
|
||||
String tokenIssuer = token.getClaimAsString(JwtClaimNames.ISS);
|
||||
if (this.issuer.equals(tokenIssuer)) {
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
} else {
|
||||
return OAuth2TokenValidatorResult.failure(INVALID_ISSUER);
|
||||
|
||||
+11
-24
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
@@ -42,7 +41,7 @@ public final class MappedJwtClaimSetConverter
|
||||
implements Converter<Map<String, Object>, Map<String, Object>> {
|
||||
|
||||
private static final Converter<Object, Collection<String>> AUDIENCE_CONVERTER = new AudienceConverter();
|
||||
private static final Converter<Object, URL> ISSUER_CONVERTER = new IssuerConverter();
|
||||
private static final Converter<Object, String> ISSUER_CONVERTER = new IssuerConverter();
|
||||
private static final Converter<Object, String> STRING_CONVERTER = new StringConverter();
|
||||
private static final Converter<Object, Instant> TEMPORAL_CONVERTER = new InstantConverter();
|
||||
|
||||
@@ -157,39 +156,27 @@ public final class MappedJwtClaimSetConverter
|
||||
* Coerces an <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4.1.1">Issuer</a> claim
|
||||
* into a {@link URL}, ignoring null values, and throwing an error if its coercion efforts fail.
|
||||
*/
|
||||
private static class IssuerConverter implements Converter<Object, URL> {
|
||||
private static class IssuerConverter implements Converter<Object, String> {
|
||||
|
||||
@Override
|
||||
public URL convert(Object source) {
|
||||
public String convert(Object source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (source instanceof URL) {
|
||||
return (URL) source;
|
||||
return ((URL) source).toExternalForm();
|
||||
}
|
||||
|
||||
if (source instanceof URI) {
|
||||
return toUrl((URI) source);
|
||||
if (source instanceof String && ((String) source).contains(":")) {
|
||||
try {
|
||||
return URI.create((String) source).toString();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Could not coerce " + source + " into a URI String", e);
|
||||
}
|
||||
}
|
||||
|
||||
return toUrl(source.toString());
|
||||
}
|
||||
|
||||
private URL toUrl(URI source) {
|
||||
try {
|
||||
return source.toURL();
|
||||
} catch (MalformedURLException e) {
|
||||
throw new IllegalStateException("Could not coerce " + source + " into a URL", e);
|
||||
}
|
||||
}
|
||||
|
||||
private URL toUrl(String source) {
|
||||
try {
|
||||
return new URL(source);
|
||||
} catch (MalformedURLException e) {
|
||||
throw new IllegalStateException("Could not coerce " + source + " into a URL", e);
|
||||
}
|
||||
return source.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+27
-8
@@ -23,9 +23,6 @@ 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;
|
||||
@@ -73,14 +70,36 @@ public class JwtIssuerValidatorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateWhenJwtIsNullThenThrowsIllegalArgumentException() {
|
||||
assertThatCode(() -> this.validator.validate(null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
public void validateWhenJwtHasNoIssuerThenReturnsError() {
|
||||
Jwt jwt = new Jwt(
|
||||
MOCK_TOKEN,
|
||||
MOCK_ISSUED_AT,
|
||||
MOCK_EXPIRES_AT,
|
||||
MOCK_HEADERS,
|
||||
Collections.singletonMap(JwtClaimNames.AUD, "https://aud"));
|
||||
|
||||
OAuth2TokenValidatorResult result = this.validator.validate(jwt);
|
||||
assertThat(result.getErrors()).isNotEmpty();
|
||||
}
|
||||
|
||||
// gh-6073
|
||||
@Test
|
||||
public void validateWhenIssuerMatchesAndIsNotAUriThenReturnsSuccess() {
|
||||
Jwt jwt = new Jwt(
|
||||
MOCK_TOKEN,
|
||||
MOCK_ISSUED_AT,
|
||||
MOCK_EXPIRES_AT,
|
||||
MOCK_HEADERS,
|
||||
Collections.singletonMap(JwtClaimNames.ISS, "issuer"));
|
||||
JwtIssuerValidator validator = new JwtIssuerValidator("issuer");
|
||||
|
||||
assertThat(validator.validate(jwt))
|
||||
.isEqualTo(OAuth2TokenValidatorResult.success());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenMalformedIssuerIsGivenThenThrowsIllegalArgumentException() {
|
||||
assertThatCode(() -> new JwtIssuerValidator("issuer"))
|
||||
public void validateWhenJwtIsNullThenThrowsIllegalArgumentException() {
|
||||
assertThatCode(() -> this.validator.validate(null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
|
||||
+25
-3
@@ -108,7 +108,7 @@ public class MappedJwtClaimSetConverterTests {
|
||||
assertThat(target.get(JwtClaimNames.AUD)).isEqualTo(Arrays.asList("audience"));
|
||||
assertThat(target.get(JwtClaimNames.EXP)).isEqualTo(Instant.ofEpochSecond(2000000000L));
|
||||
assertThat(target.get(JwtClaimNames.IAT)).isEqualTo(Instant.ofEpochSecond(1000000000L));
|
||||
assertThat(target.get(JwtClaimNames.ISS)).isEqualTo(new URL("https://any.url"));
|
||||
assertThat(target.get(JwtClaimNames.ISS)).isEqualTo("https://any.url");
|
||||
assertThat(target.get(JwtClaimNames.NBF)).isEqualTo(Instant.ofEpochSecond(1000000000L));
|
||||
assertThat(target.get(JwtClaimNames.SUB)).isEqualTo("1234");
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public class MappedJwtClaimSetConverterTests {
|
||||
assertThat(target.get(JwtClaimNames.AUD)).isEqualTo(Arrays.asList("audience"));
|
||||
assertThat(target.get(JwtClaimNames.EXP)).isEqualTo(Instant.ofEpochSecond(2000000000L));
|
||||
assertThat(target.get(JwtClaimNames.IAT)).isEqualTo(Instant.ofEpochSecond(1000000000L));
|
||||
assertThat(target.get(JwtClaimNames.ISS)).isEqualTo(new URL("https://any.url"));
|
||||
assertThat(target.get(JwtClaimNames.ISS)).isEqualTo("https://any.url");
|
||||
assertThat(target.get(JwtClaimNames.NBF)).isEqualTo(Instant.ofEpochSecond(1000000000L));
|
||||
assertThat(target.get(JwtClaimNames.SUB)).isEqualTo("1234");
|
||||
}
|
||||
@@ -196,7 +196,7 @@ public class MappedJwtClaimSetConverterTests {
|
||||
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
|
||||
.withDefaults(Collections.emptyMap());
|
||||
|
||||
Map<String, Object> badIssuer = Collections.singletonMap(JwtClaimNames.ISS, "badly-formed-iss");
|
||||
Map<String, Object> badIssuer = Collections.singletonMap(JwtClaimNames.ISS, "https://badly formed iss");
|
||||
assertThatCode(() -> converter.convert(badIssuer)).isInstanceOf(IllegalStateException.class);
|
||||
|
||||
Map<String, Object> badIssuedAt = Collections.singletonMap(JwtClaimNames.IAT, "badly-formed-iat");
|
||||
@@ -209,6 +209,28 @@ public class MappedJwtClaimSetConverterTests {
|
||||
assertThatCode(() -> converter.convert(badNotBefore)).isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
// gh-6073
|
||||
@Test
|
||||
public void convertWhenIssuerIsNotAUriThenConvertsToString() {
|
||||
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
|
||||
.withDefaults(Collections.emptyMap());
|
||||
|
||||
Map<String, Object> nonUriIssuer = Collections.singletonMap(JwtClaimNames.ISS, "issuer");
|
||||
Map<String, Object> target = converter.convert(nonUriIssuer);
|
||||
assertThat(target.get(JwtClaimNames.ISS)).isEqualTo("issuer");
|
||||
}
|
||||
|
||||
// gh-6073
|
||||
@Test
|
||||
public void convertWhenIssuerIsOfTypeURLThenConvertsToString() throws Exception {
|
||||
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
|
||||
.withDefaults(Collections.emptyMap());
|
||||
|
||||
Map<String, Object> issuer = Collections.singletonMap(JwtClaimNames.ISS, new URL("https://issuer"));
|
||||
Map<String, Object> target = converter.convert(issuer);
|
||||
assertThat(target.get(JwtClaimNames.ISS)).isEqualTo("https://issuer");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructWhenAnyParameterIsNullThenIllegalArgumentException() {
|
||||
assertThatCode(() -> new MappedJwtClaimSetConverter(null))
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
ext['thymeleaf.version'] = '3.0.9.RELEASE'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-security-config')
|
||||
compile project(':spring-security-web')
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-web'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
testCompile 'org.springframework.boot:spring-boot-starter-test'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
<head>
|
||||
<title>Hello Spring Security</title>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
ext['thymeleaf.version'] = '3.0.9.RELEASE'
|
||||
|
||||
dependencies {
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-web'
|
||||
|
||||
+1
-3
@@ -1,14 +1,12 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
ext['thymeleaf.version'] = '3.0.9.RELEASE'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-security-config')
|
||||
compile project(':spring-security-oauth2-client')
|
||||
compile project(':spring-security-oauth2-jose')
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
testCompile 'net.sourceforge.htmlunit:htmlunit'
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
ext['thymeleaf.version'] = '3.0.9.RELEASE'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-security-config')
|
||||
compile project(':spring-security-oauth2-client')
|
||||
compile project(':spring-security-oauth2-jose')
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-web'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
testCompile 'net.sourceforge.htmlunit:htmlunit'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
<head>
|
||||
<title>Spring Security - OAuth 2.0 Login</title>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -40,7 +40,7 @@ spring:
|
||||
client-id: replace-with-client-id
|
||||
client-secret: replace-with-client-secret
|
||||
provider: github
|
||||
scopes: read:user,public_repo
|
||||
scope: read:user,public_repo
|
||||
----
|
||||
+
|
||||
.OAuth Client properties
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ dependencies {
|
||||
compile project(':spring-security-oauth2-jose')
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
compile 'io.projectreactor.netty:reactor-netty'
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
|
||||
@@ -16,6 +16,6 @@ spring:
|
||||
client-id: replace-with-client-id
|
||||
client-secret: replace-with-client-secret
|
||||
provider: github
|
||||
scopes: read:user,public_repo
|
||||
scope: read:user,public_repo
|
||||
|
||||
resource-uri: https://api.github.com/user/repos
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
<head>
|
||||
<title>OAuth2 WebClient Showcase</title>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -40,7 +40,7 @@ spring:
|
||||
client-id: replace-with-client-id
|
||||
client-secret: replace-with-client-secret
|
||||
provider: github
|
||||
scopes: read:user,public_repo
|
||||
scope: read:user,public_repo
|
||||
----
|
||||
+
|
||||
.OAuth Client properties
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
ext['thymeleaf.version'] = '3.0.9.RELEASE'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-security-config')
|
||||
compile project(':spring-security-oauth2-client')
|
||||
@@ -9,7 +7,7 @@ dependencies {
|
||||
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 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
compile 'io.projectreactor.netty:reactor-netty'
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
|
||||
@@ -16,6 +16,6 @@ spring:
|
||||
client-id: replace-with-client-id
|
||||
client-secret: replace-with-client-secret
|
||||
provider: github
|
||||
scopes: read:user,public_repo
|
||||
scope: read:user,public_repo
|
||||
|
||||
resource-uri: https://api.github.com/user/repos
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
<head>
|
||||
<title>OAuth2 WebClient Showcase</title>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -22,6 +22,8 @@ dependencies {
|
||||
compile project(':spring-security-web')
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.0.4.RELEASE'
|
||||
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
testCompile 'org.springframework.boot:spring-boot-starter-test'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user