Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ae7ceef70 | |||
| b3c7f0a79f | |||
| 4df4f9a63d | |||
| 9d25c2d2f8 | |||
| 94b116c8c8 | |||
| 3c6f08ede0 | |||
| 9a94234dae | |||
| 017044bf93 | |||
| ae99097723 | |||
| acb7e63cf7 | |||
| 0256439a50 | |||
| c8f72a1b87 | |||
| 332e8ce07a | |||
| 7aea459535 | |||
| c91389ff8b | |||
| 6de345b972 | |||
| 19f08cbedb | |||
| f82e435aaf | |||
| a44225d334 | |||
| 5decfb1ece | |||
| a24d67375b | |||
| 3a46ba8a85 | |||
| a4851095df | |||
| 633e5b85b4 | |||
| bfa5830e3d | |||
| e43fab518c | |||
| c9f676739b | |||
| b7ce65b284 | |||
| 67d561b5f7 | |||
| 4dd2b1dfe1 | |||
| aa28a0b453 | |||
| 8145cb557b | |||
| ef4109358a | |||
| 6d6fd09665 |
+1
-1
@@ -47,7 +47,7 @@ public class ServiceAuthenticationDetailsSource implements
|
||||
// ===================================================================================================
|
||||
|
||||
/**
|
||||
* Creates an implementation that uses the specified ServiceProperites and the default
|
||||
* Creates an implementation that uses the specified ServiceProperties and the default
|
||||
* CAS artifactParameterName.
|
||||
*
|
||||
* @param serviceProperties The ServiceProperties to use to construct the serviceUrl.
|
||||
|
||||
+1
-1
@@ -723,7 +723,7 @@ public final class HttpSecurity extends
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @return the {@link ServletApiConfigurer} for further customizations
|
||||
* @return the {@link CsrfConfigurer} for further customizations
|
||||
* @throws Exception
|
||||
*/
|
||||
public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
|
||||
|
||||
+1
-1
@@ -171,7 +171,7 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecur
|
||||
|
||||
/**
|
||||
* Specifies the {@link AuthenticationSuccessHandler} to be used. The default is
|
||||
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properites
|
||||
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properties
|
||||
* set.
|
||||
*
|
||||
* @param successHandler the {@link AuthenticationSuccessHandler}.
|
||||
|
||||
+19
-19
@@ -160,6 +160,25 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
|
||||
@Override
|
||||
public void init(H http) throws Exception {
|
||||
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.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();
|
||||
Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter =
|
||||
this.jwtConfigurer.getJwtAuthenticationConverter();
|
||||
|
||||
JwtAuthenticationProvider provider =
|
||||
new JwtAuthenticationProvider(decoder);
|
||||
provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
|
||||
provider = postProcess(provider);
|
||||
|
||||
http.authenticationProvider(provider);
|
||||
|
||||
registerDefaultAccessDeniedHandler(http);
|
||||
registerDefaultEntryPoint(http);
|
||||
registerDefaultCsrfOverride(http);
|
||||
@@ -179,25 +198,6 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
||||
filter = postProcess(filter);
|
||||
|
||||
http.addFilter(filter);
|
||||
|
||||
if ( this.jwtConfigurer == null ) {
|
||||
throw new IllegalStateException("Jwt is the only supported format for bearer tokens " +
|
||||
"in Spring Security and no Jwt configuration was found. Make sure to specify " +
|
||||
"a jwk set uri by doing http.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();
|
||||
Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter =
|
||||
this.jwtConfigurer.getJwtAuthenticationConverter();
|
||||
|
||||
JwtAuthenticationProvider provider =
|
||||
new JwtAuthenticationProvider(decoder);
|
||||
provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
|
||||
provider = postProcess(provider);
|
||||
|
||||
http.authenticationProvider(provider);
|
||||
}
|
||||
|
||||
public class JwtConfigurer {
|
||||
|
||||
+1
-1
@@ -208,7 +208,7 @@ public class GlobalMethodSecurityBeanDefinitionParser implements BeanDefinitionP
|
||||
pc.registerBeanComponent(new BeanComponentDefinition(
|
||||
expressionHandler, expressionHandlerRef));
|
||||
logger.info("Expressions were enabled for method security but no SecurityExpressionHandler was configured. "
|
||||
+ "All hasPermision() expressions will evaluate to false.");
|
||||
+ "All hasPermission() expressions will evaluate to false.");
|
||||
}
|
||||
|
||||
BeanDefinitionBuilder expressionPreAdviceBldr = BeanDefinitionBuilder
|
||||
|
||||
+46
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -621,20 +621,56 @@ public class ServerHttpSecurity {
|
||||
authenticationFilter.setAuthenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/login?error"));
|
||||
authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
|
||||
|
||||
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
|
||||
MediaType.TEXT_HTML);
|
||||
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||
Map<String, String> urlToText = http.oauth2Login.getLinks();
|
||||
if (urlToText.size() == 1) {
|
||||
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint(urlToText.keySet().iterator().next())));
|
||||
} else {
|
||||
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint("/login")));
|
||||
}
|
||||
setDefaultEntryPoints(http);
|
||||
|
||||
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
|
||||
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||
}
|
||||
|
||||
private void setDefaultEntryPoints(ServerHttpSecurity http) {
|
||||
String defaultLoginPage = "/login";
|
||||
Map<String, String> urlToText = http.oauth2Login.getLinks();
|
||||
String providerLoginPage = null;
|
||||
if (urlToText.size() == 1) {
|
||||
providerLoginPage = urlToText.keySet().iterator().next();
|
||||
}
|
||||
|
||||
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
|
||||
MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"),
|
||||
MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
|
||||
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||
|
||||
ServerWebExchangeMatcher xhrMatcher = exchange -> {
|
||||
if (exchange.getRequest().getHeaders().getOrDefault("X-Requested-With", Collections.emptyList()).contains("XMLHttpRequest")) {
|
||||
return ServerWebExchangeMatcher.MatchResult.match();
|
||||
}
|
||||
return ServerWebExchangeMatcher.MatchResult.notMatch();
|
||||
};
|
||||
ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
|
||||
|
||||
ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(
|
||||
notXhrMatcher, htmlMatcher);
|
||||
|
||||
if (providerLoginPage != null) {
|
||||
ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher(defaultLoginPage);
|
||||
ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher("/favicon.ico");
|
||||
ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher(
|
||||
new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
|
||||
|
||||
ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(
|
||||
notXhrMatcher, new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher));
|
||||
RedirectServerAuthenticationEntryPoint entryPoint =
|
||||
new RedirectServerAuthenticationEntryPoint(providerLoginPage);
|
||||
entryPoint.setRequestCache(http.requestCache.requestCache);
|
||||
http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint));
|
||||
}
|
||||
|
||||
RedirectServerAuthenticationEntryPoint defaultEntryPoint =
|
||||
new RedirectServerAuthenticationEntryPoint(defaultLoginPage);
|
||||
defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
|
||||
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
|
||||
}
|
||||
|
||||
private ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() {
|
||||
return new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}");
|
||||
}
|
||||
|
||||
+30
-1
@@ -298,6 +298,18 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Bearer"));
|
||||
}
|
||||
|
||||
// gh-8031
|
||||
@Test
|
||||
public void getWhenAnonymousDisabledThenAllows() throws Exception {
|
||||
this.spring.register(JwtDecoderConfig.class, AnonymousDisabledConfig.class).autowire();
|
||||
JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
|
||||
when(decoder.decode(anyString())).thenReturn(JWT);
|
||||
|
||||
this.mvc.perform(get("/authenticated")
|
||||
.with(bearerToken("token")))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getWhenUsingDefaultsWithNoBearerTokenThenUnauthorized()
|
||||
throws Exception {
|
||||
@@ -652,7 +664,8 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
|
||||
@Test
|
||||
public void getBearerTokenResolverWhenDuplicateResolverBeansThenWiringException() {
|
||||
assertThatCode(() -> this.spring.register(MultipleBearerTokenResolverBeansConfig.class).autowire())
|
||||
assertThatCode(() -> this.spring
|
||||
.register(JwtDecoderConfig.class, MultipleBearerTokenResolverBeansConfig.class).autowire())
|
||||
.isInstanceOf(BeanCreationException.class)
|
||||
.hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class);
|
||||
}
|
||||
@@ -1097,6 +1110,22 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class AnonymousDisabledConfig extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.anonymous().disable()
|
||||
.oauth2ResourceServer()
|
||||
.jwt();
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
static class MethodSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
+31
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,8 +26,10 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
@@ -61,10 +63,12 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
|
||||
import org.springframework.web.server.WebHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
@@ -79,6 +83,8 @@ public class OAuth2LoginTests {
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
|
||||
private WebTestClient client;
|
||||
|
||||
@Autowired
|
||||
private WebFilterChainProxy springSecurity;
|
||||
|
||||
@@ -94,6 +100,14 @@ public class OAuth2LoginTests {
|
||||
.clientSecret("secret")
|
||||
.build();
|
||||
|
||||
@Autowired
|
||||
public void setApplicationContext(ApplicationContext context) {
|
||||
if (context.getBeanNamesForType(WebHandler.class).length > 0) {
|
||||
this.client = WebTestClient.bindToApplicationContext(context)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() {
|
||||
this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class).autowire();
|
||||
@@ -140,6 +154,22 @@ public class OAuth2LoginTests {
|
||||
assertThat(driver.getCurrentUrl()).startsWith("https://github.com/login/oauth/authorize");
|
||||
}
|
||||
|
||||
// gh-8118
|
||||
@Test
|
||||
public void defaultLoginPageWithSingleClientRegistrationAndXhrRequestThenDoesNotRedirectForAuthorization() {
|
||||
this.spring.register(OAuth2LoginWithSingleClientRegistrations.class, WebFluxConfig.class).autowire();
|
||||
|
||||
this.client.get()
|
||||
.uri("/")
|
||||
.header("X-Requested-With", "XMLHttpRequest")
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection()
|
||||
.expectHeader().valueEquals(HttpHeaders.LOCATION, "/login");
|
||||
}
|
||||
|
||||
@EnableWebFlux
|
||||
static class WebFluxConfig { }
|
||||
|
||||
@EnableWebFluxSecurity
|
||||
static class OAuth2LoginWithSingleClientRegistrations {
|
||||
@Bean
|
||||
|
||||
-2
@@ -17,7 +17,6 @@ package org.springframework.security.access.expression;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.security.access.PermissionEvaluator;
|
||||
@@ -158,7 +157,6 @@ public abstract class SecurityExpressionRoot implements SecurityExpressionOperat
|
||||
|
||||
private Set<String> getAuthoritySet() {
|
||||
if (roles == null) {
|
||||
roles = new HashSet<>();
|
||||
Collection<? extends GrantedAuthority> userAuthorities = authentication
|
||||
.getAuthorities();
|
||||
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ import org.springframework.util.Assert;
|
||||
* </property>
|
||||
* </pre>
|
||||
*
|
||||
* A configuration note: The JaasAuthenticationProvider uses the security properites
|
||||
* A configuration note: The JaasAuthenticationProvider uses the security properties
|
||||
* "login.config.url.X" to configure jaas. If you would like to customize the way Jaas
|
||||
* gets configured, create a subclass of this and override the
|
||||
* {@link #configureJaas(Resource)} method.
|
||||
|
||||
@@ -39,9 +39,6 @@ public class Encryptors {
|
||||
* not be shared
|
||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||
* key
|
||||
*
|
||||
* @see #standard(CharSequence, CharSequence) which uses the slightly weaker CBC mode
|
||||
* (instead of GCM)
|
||||
*/
|
||||
public static BytesEncryptor stronger(CharSequence password, CharSequence salt) {
|
||||
return new AesBytesEncryptor(password.toString(), salt,
|
||||
@@ -55,11 +52,19 @@ public class Encryptors {
|
||||
* provided salt is expected to be hex-encoded; it should be random and at least 8
|
||||
* bytes in length. Also applies a random 16 byte initialization vector to ensure each
|
||||
* encrypted message will be unique. Requires Java 6.
|
||||
* NOTE: This mode is not
|
||||
* <a href="https://en.wikipedia.org/wiki/Authenticated_encryption">authenticated</a>
|
||||
* and does not provide any guarantees about the authenticity of the data.
|
||||
* For a more secure alternative, users should prefer
|
||||
* {@link #stronger(CharSequence, CharSequence)}.
|
||||
*
|
||||
* @param password the password used to generate the encryptor's secret key; should
|
||||
* not be shared
|
||||
* @param salt a hex-encoded, random, site-global salt value to use to generate the
|
||||
* key
|
||||
*
|
||||
* @see #stronger(CharSequence, CharSequence) which uses the significatly more secure
|
||||
* GCM (instead of CBC)
|
||||
*/
|
||||
public static BytesEncryptor standard(CharSequence password, CharSequence salt) {
|
||||
return new AesBytesEncryptor(password.toString(), salt,
|
||||
|
||||
@@ -17,14 +17,16 @@ Encryptors are thread-safe.
|
||||
|
||||
[[spring-security-crypto-encryption-bytes]]
|
||||
==== BytesEncryptor
|
||||
Use the Encryptors.standard factory method to construct a "standard" BytesEncryptor:
|
||||
Use the `Encryptors.stronger` factory method to construct a BytesEncryptor:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Encryptors.standard("password", "salt");
|
||||
Encryptors.stronger("password", "salt");
|
||||
----
|
||||
|
||||
The "standard" encryption method is 256-bit AES using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
|
||||
The "stronger" encryption method creates an encryptor using 256 bit AES encryption with
|
||||
Galois Counter Mode (GCM).
|
||||
It derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2).
|
||||
This method requires Java 6.
|
||||
The password used to generate the SecretKey should be kept in a secure place and not be shared.
|
||||
The salt is used to prevent dictionary attacks against the key in the event your encrypted data is compromised.
|
||||
@@ -38,6 +40,11 @@ Such a salt may be generated using a KeyGenerator:
|
||||
String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded
|
||||
----
|
||||
|
||||
Users may also use the `standard` encryption method, which is 256-bit AES in Cipher Block Chaining (CBC) Mode.
|
||||
This mode is not https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated] and does not provide any
|
||||
guarantees about the authenticity of the data.
|
||||
For a more secure alternative, users should prefer `Encryptors.stronger`.
|
||||
|
||||
[[spring-security-crypto-encryption-text]]
|
||||
==== TextEncryptor
|
||||
Use the Encryptors.text factory method to construct a standard TextEncryptor:
|
||||
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
gaeVersion=1.9.71
|
||||
springBootVersion=2.1.12.RELEASE
|
||||
version=5.1.8.RELEASE
|
||||
gaeVersion=1.9.79
|
||||
springBootVersion=2.1.13.RELEASE
|
||||
version=5.1.9.RELEASE
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
if (!project.hasProperty('reactorVersion')) {
|
||||
ext.reactorVersion = 'Californium-SR15'
|
||||
ext.reactorVersion = 'Californium-SR17'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springVersion')) {
|
||||
ext.springVersion = '5.1.13.RELEASE'
|
||||
ext.springVersion = '5.1.14.RELEASE'
|
||||
}
|
||||
|
||||
if (!project.hasProperty('springDataVersion')) {
|
||||
ext.springDataVersion = 'Lovelace-SR15'
|
||||
ext.springDataVersion = 'Lovelace-SR16'
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
@@ -18,16 +18,16 @@ dependencyManagement {
|
||||
}
|
||||
dependencies {
|
||||
dependency 'cglib:cglib-nodep:3.2.12'
|
||||
dependency 'com.squareup.okhttp3:mockwebserver:3.12.8'
|
||||
dependency 'com.squareup.okhttp3:mockwebserver:3.12.10'
|
||||
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.5'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.5'
|
||||
dependency 'org.powermock:powermock-core:2.0.5'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.5'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.5'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.5'
|
||||
dependency 'org.powermock:powermock-api-mockito2:2.0.6'
|
||||
dependency 'org.powermock:powermock-api-support:2.0.6'
|
||||
dependency 'org.powermock:powermock-core:2.0.6'
|
||||
dependency 'org.powermock:powermock-module-junit4-common:2.0.6'
|
||||
dependency 'org.powermock:powermock-module-junit4:2.0.6'
|
||||
dependency 'org.powermock:powermock-reflect:2.0.6'
|
||||
dependency 'org.python:jython:2.5.3'
|
||||
dependency 'org.spockframework:spock-core:1.0-groovy-2.4'
|
||||
dependency 'org.spockframework:spock-spring:1.0-groovy-2.4'
|
||||
@@ -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.12.8'
|
||||
dependency 'com.squareup.okhttp3:okhttp:3.12.10'
|
||||
dependency 'com.squareup.okio:okio:1.13.0'
|
||||
dependency 'com.sun.xml.bind:jaxb-core:2.3.0.1'
|
||||
dependency 'com.sun.xml.bind:jaxb-impl:2.3.2'
|
||||
dependency 'com.unboundid:unboundid-ldapsdk:4.0.11'
|
||||
dependency 'com.unboundid:unboundid-ldapsdk:4.0.14'
|
||||
dependency 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1'
|
||||
dependency 'commons-cli:commons-cli:1.4'
|
||||
dependency 'commons-codec:commons-codec:1.11'
|
||||
@@ -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.11'
|
||||
dependency 'org.apache.httpcomponents:httpclient:4.5.12'
|
||||
dependency 'org.apache.httpcomponents:httpcore:4.4.8'
|
||||
dependency 'org.apache.httpcomponents:httpmime:4.5.3'
|
||||
dependency 'org.apache.mina:mina-core:2.0.0-M6'
|
||||
@@ -147,21 +147,21 @@ dependencyManagement {
|
||||
dependency 'org.attoparser:attoparser:2.0.4.RELEASE'
|
||||
dependency 'org.bouncycastle:bcpkix-jdk15on:1.64'
|
||||
dependency 'org.bouncycastle:bcprov-jdk15on:1.58'
|
||||
dependency 'org.codehaus.groovy:groovy-all:2.4.17'
|
||||
dependency 'org.codehaus.groovy:groovy-json:2.4.17'
|
||||
dependency 'org.codehaus.groovy:groovy:2.4.14'
|
||||
dependency 'org.codehaus.groovy:groovy-all:2.4.19'
|
||||
dependency 'org.codehaus.groovy:groovy-json:2.4.19'
|
||||
dependency 'org.codehaus.groovy:groovy:2.4.19'
|
||||
dependency 'org.eclipse.jdt:ecj:3.12.3'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-api:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-client:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-http:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-io:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-security:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-server:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-servlet:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-util:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty:jetty-xml:9.4.19.v20190610'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-api:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-client:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty.websocket:websocket-common:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-client:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-http:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-io:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-security:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-server:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-servlet:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-util:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.jetty:jetty-xml:9.4.27.v20200227'
|
||||
dependency 'org.eclipse.persistence:javax.persistence:2.2.1'
|
||||
dependency 'org.gebish:geb-ast:0.10.0'
|
||||
dependency 'org.gebish:geb-core:0.10.0'
|
||||
@@ -170,9 +170,9 @@ dependencyManagement {
|
||||
dependency 'org.hamcrest:hamcrest-core:1.3'
|
||||
dependency 'org.hibernate.common:hibernate-commons-annotations:5.0.1.Final'
|
||||
dependency 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
|
||||
dependency 'org.hibernate:hibernate-core:5.2.17.Final'
|
||||
dependency 'org.hibernate:hibernate-core:5.2.18.Final'
|
||||
dependency 'org.hibernate:hibernate-entitymanager:5.3.15.Final'
|
||||
dependency 'org.hibernate:hibernate-validator:6.0.18.Final'
|
||||
dependency 'org.hibernate:hibernate-validator:6.0.19.Final'
|
||||
dependency 'org.hsqldb:hsqldb:2.4.1'
|
||||
dependency 'org.jasig.cas.client:cas-client-core:3.5.1'
|
||||
dependency 'org.javassist:javassist:3.22.0-CR2'
|
||||
@@ -183,7 +183,7 @@ dependencyManagement {
|
||||
dependency 'org.objenesis:objenesis:2.6'
|
||||
dependency 'org.openid4java:openid4java-nodeps:0.9.6'
|
||||
dependency 'org.ow2.asm:asm:6.2.1'
|
||||
dependency 'org.reactivestreams:reactive-streams:1.0.1'
|
||||
dependency 'org.reactivestreams:reactive-streams:1.0.3'
|
||||
dependency 'org.seleniumhq.selenium:htmlunit-driver:2.33.3'
|
||||
dependency 'org.seleniumhq.selenium:selenium-api:3.141.59'
|
||||
dependency 'org.seleniumhq.selenium:selenium-java:3.141.59'
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -73,7 +73,8 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
|
||||
authorizationCodeAuthentication.getClientRegistration(),
|
||||
authorizationCodeAuthentication.getAuthorizationExchange(),
|
||||
accessTokenResponse.getAccessToken(),
|
||||
accessTokenResponse.getRefreshToken());
|
||||
accessTokenResponse.getRefreshToken(),
|
||||
accessTokenResponse.getAdditionalParameters());
|
||||
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
|
||||
|
||||
return authenticationResult;
|
||||
|
||||
+17
-23
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,7 +28,6 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -60,7 +59,7 @@ import java.util.Map;
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
|
||||
*/
|
||||
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
|
||||
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
|
||||
private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
|
||||
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
|
||||
private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
|
||||
|
||||
@@ -74,59 +73,54 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
|
||||
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
|
||||
|
||||
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
|
||||
Assert.notNull(userService, "userService cannot be null");
|
||||
this.accessTokenResponseClient = accessTokenResponseClient;
|
||||
this.authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient);
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2LoginAuthenticationToken authorizationCodeAuthentication =
|
||||
OAuth2LoginAuthenticationToken loginAuthenticationToken =
|
||||
(OAuth2LoginAuthenticationToken) authentication;
|
||||
|
||||
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
// scope
|
||||
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
|
||||
if (authorizationCodeAuthentication.getAuthorizationExchange()
|
||||
if (loginAuthenticationToken.getAuthorizationExchange()
|
||||
.getAuthorizationRequest().getScopes().contains("openid")) {
|
||||
// This is an OpenID Connect Authentication Request so return null
|
||||
// and let OidcAuthorizationCodeAuthenticationProvider handle it instead
|
||||
return null;
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse;
|
||||
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
|
||||
try {
|
||||
OAuth2AuthorizationExchangeValidator.validate(
|
||||
authorizationCodeAuthentication.getAuthorizationExchange());
|
||||
|
||||
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
|
||||
new OAuth2AuthorizationCodeGrantRequest(
|
||||
authorizationCodeAuthentication.getClientRegistration(),
|
||||
authorizationCodeAuthentication.getAuthorizationExchange()));
|
||||
|
||||
authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
|
||||
.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
|
||||
loginAuthenticationToken.getClientRegistration(),
|
||||
loginAuthenticationToken.getAuthorizationExchange()));
|
||||
} catch (OAuth2AuthorizationException ex) {
|
||||
OAuth2Error oauth2Error = ex.getError();
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
|
||||
Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
|
||||
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
|
||||
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
|
||||
|
||||
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
|
||||
authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters));
|
||||
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
|
||||
|
||||
Collection<? extends GrantedAuthority> mappedAuthorities =
|
||||
this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
|
||||
|
||||
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
|
||||
authorizationCodeAuthentication.getClientRegistration(),
|
||||
authorizationCodeAuthentication.getAuthorizationExchange(),
|
||||
loginAuthenticationToken.getClientRegistration(),
|
||||
loginAuthenticationToken.getAuthorizationExchange(),
|
||||
oauth2User,
|
||||
mappedAuthorities,
|
||||
accessToken,
|
||||
accessTokenResponse.getRefreshToken());
|
||||
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
|
||||
authorizationCodeAuthenticationToken.getRefreshToken());
|
||||
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
|
||||
|
||||
return authenticationResult;
|
||||
}
|
||||
|
||||
+33
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -41,6 +41,7 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -48,6 +49,11 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
|
||||
@@ -132,24 +138,39 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
if (this.shouldProcessAuthorizationResponse(request)) {
|
||||
this.processAuthorizationResponse(request, response);
|
||||
if (matchesAuthorizationResponse(request)) {
|
||||
processAuthorizationResponse(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private boolean shouldProcessAuthorizationResponse(HttpServletRequest request) {
|
||||
private boolean matchesAuthorizationResponse(HttpServletRequest request) {
|
||||
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
|
||||
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
|
||||
return false;
|
||||
}
|
||||
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
|
||||
if (authorizationRequest == null) {
|
||||
return false;
|
||||
}
|
||||
String requestUrl = UrlUtils.buildFullRequestUrl(request.getScheme(), request.getServerName(),
|
||||
request.getServerPort(), request.getRequestURI(), null);
|
||||
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
|
||||
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&
|
||||
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
|
||||
|
||||
// Compare redirect_uri
|
||||
UriComponents requestUri = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request)).build();
|
||||
UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri()).build();
|
||||
Set<Map.Entry<String, List<String>>> requestUriParameters = new LinkedHashSet<>(requestUri.getQueryParams().entrySet());
|
||||
Set<Map.Entry<String, List<String>>> redirectUriParameters = new LinkedHashSet<>(redirectUri.getQueryParams().entrySet());
|
||||
// Remove the additional request parameters (if any) from the authorization response (request)
|
||||
// before doing an exact comparison with the authorizationRequest.getRedirectUri() parameters (if any)
|
||||
requestUriParameters.retainAll(redirectUriParameters);
|
||||
|
||||
if (Objects.equals(requestUri.getScheme(), redirectUri.getScheme()) &&
|
||||
Objects.equals(requestUri.getUserInfo(), redirectUri.getUserInfo()) &&
|
||||
Objects.equals(requestUri.getHost(), redirectUri.getHost()) &&
|
||||
Objects.equals(requestUri.getPort(), redirectUri.getPort()) &&
|
||||
Objects.equals(requestUri.getPath(), redirectUri.getPath()) &&
|
||||
Objects.equals(requestUriParameters.toString(), redirectUriParameters.toString())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -165,10 +186,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
|
||||
|
||||
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
|
||||
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
||||
.replaceQuery(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
String redirectUri = UrlUtils.buildFullRequestUrl(request);
|
||||
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
|
||||
|
||||
OAuth2AuthorizationCodeAuthenticationToken authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken(
|
||||
@@ -183,7 +201,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
|
||||
} catch (OAuth2AuthorizationException ex) {
|
||||
OAuth2Error error = ex.getError();
|
||||
UriComponentsBuilder uriBuilder = UriComponentsBuilder
|
||||
.fromUriString(authorizationResponse.getRedirectUri())
|
||||
.fromUriString(authorizationRequest.getRedirectUri())
|
||||
.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
|
||||
if (!StringUtils.isEmpty(error.getDescription())) {
|
||||
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
|
||||
@@ -206,7 +224,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
|
||||
|
||||
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request, response);
|
||||
|
||||
String redirectUrl = authorizationResponse.getRedirectUri();
|
||||
String redirectUrl = authorizationRequest.getRedirectUri();
|
||||
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
|
||||
if (savedRequest != null) {
|
||||
redirectUrl = savedRequest.getRedirectUrl();
|
||||
|
||||
+40
-19
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -37,13 +37,20 @@ import org.springframework.security.web.server.authentication.ServerAuthenticati
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
|
||||
* which handles the processing of the OAuth 2.0 Authorization Response.
|
||||
@@ -138,10 +145,10 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return this.requiresAuthenticationMatcher.matches(exchange)
|
||||
.filter( matchResult -> matchResult.isMatch())
|
||||
.flatMap( matchResult -> this.authenticationConverter.convert(exchange))
|
||||
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||
.flatMap(matchResult -> this.authenticationConverter.convert(exchange))
|
||||
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
|
||||
.flatMap( token -> authenticate(exchange, chain, token));
|
||||
.flatMap(token -> authenticate(exchange, chain, token));
|
||||
}
|
||||
|
||||
private Mono<Void> authenticate(ServerWebExchange exchange,
|
||||
@@ -171,20 +178,34 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter {
|
||||
}
|
||||
|
||||
private Mono<ServerWebExchangeMatcher.MatchResult> matchesAuthorizationResponse(ServerWebExchange exchange) {
|
||||
return this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
|
||||
.flatMap(authorizationRequest -> {
|
||||
String requestUrl = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
|
||||
.query(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
|
||||
if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&
|
||||
OAuth2AuthorizationResponseUtils.isAuthorizationResponse(queryParams)) {
|
||||
return ServerWebExchangeMatcher.MatchResult.match();
|
||||
}
|
||||
return ServerWebExchangeMatcher.MatchResult.notMatch();
|
||||
})
|
||||
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||
return Mono.just(exchange)
|
||||
.filter(exch -> OAuth2AuthorizationResponseUtils.isAuthorizationResponse(exch.getRequest().getQueryParams()))
|
||||
.flatMap(exch -> this.authorizationRequestRepository.loadAuthorizationRequest(exchange)
|
||||
.flatMap(authorizationRequest ->
|
||||
matchesRedirectUri(exch.getRequest().getURI(), authorizationRequest.getRedirectUri())))
|
||||
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
|
||||
}
|
||||
|
||||
private static Mono<ServerWebExchangeMatcher.MatchResult> matchesRedirectUri(
|
||||
URI authorizationResponseUri, String authorizationRequestRedirectUri) {
|
||||
UriComponents requestUri = UriComponentsBuilder.fromUri(authorizationResponseUri).build();
|
||||
UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequestRedirectUri).build();
|
||||
Set<Map.Entry<String, List<String>>> requestUriParameters =
|
||||
new LinkedHashSet<>(requestUri.getQueryParams().entrySet());
|
||||
Set<Map.Entry<String, List<String>>> redirectUriParameters =
|
||||
new LinkedHashSet<>(redirectUri.getQueryParams().entrySet());
|
||||
// Remove the additional request parameters (if any) from the authorization response (request)
|
||||
// before doing an exact comparison with the authorizationRequest.getRedirectUri() parameters (if any)
|
||||
requestUriParameters.retainAll(redirectUriParameters);
|
||||
|
||||
if (Objects.equals(requestUri.getScheme(), redirectUri.getScheme()) &&
|
||||
Objects.equals(requestUri.getUserInfo(), redirectUri.getUserInfo()) &&
|
||||
Objects.equals(requestUri.getHost(), redirectUri.getHost()) &&
|
||||
Objects.equals(requestUri.getPort(), redirectUri.getPort()) &&
|
||||
Objects.equals(requestUri.getPath(), redirectUri.getPath()) &&
|
||||
Objects.equals(requestUriParameters.toString(), redirectUriParameters.toString())) {
|
||||
return ServerWebExchangeMatcher.MatchResult.match();
|
||||
}
|
||||
return ServerWebExchangeMatcher.MatchResult.notMatch();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,7 +28,6 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -103,14 +102,10 @@ public class ServerOAuth2AuthorizationCodeAuthenticationTokenConverter
|
||||
}
|
||||
|
||||
private static OAuth2AuthorizationResponse convertResponse(ServerWebExchange exchange) {
|
||||
MultiValueMap<String, String> queryParams = exchange.getRequest()
|
||||
.getQueryParams();
|
||||
String redirectUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
|
||||
.query(null)
|
||||
.build()
|
||||
.toUriString();
|
||||
|
||||
return OAuth2AuthorizationResponseUtils
|
||||
.convert(queryParams, redirectUri);
|
||||
.convert(exchange.getRequest().getQueryParams(), redirectUri);
|
||||
}
|
||||
}
|
||||
|
||||
+28
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,6 +15,10 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.client.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -33,13 +37,12 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExch
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses.accessTokenResponse;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2AuthorizationCodeAuthenticationProvider}.
|
||||
@@ -130,4 +133,26 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
|
||||
assertThat(authenticationResult.getAccessToken()).isEqualTo(accessToken);
|
||||
assertThat(authenticationResult.getRefreshToken()).isEqualTo(refreshToken);
|
||||
}
|
||||
|
||||
// gh-5368
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationSuccessResponseThenAdditionalParametersIncluded() {
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put("param1", "value1");
|
||||
additionalParameters.put("param2", "value2");
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().additionalParameters(additionalParameters)
|
||||
.build();
|
||||
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
|
||||
|
||||
OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest,
|
||||
this.authorizationResponse);
|
||||
|
||||
OAuth2AuthorizationCodeAuthenticationToken authentication = (OAuth2AuthorizationCodeAuthenticationToken) this.authenticationProvider
|
||||
.authenticate(
|
||||
new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, authorizationExchange));
|
||||
|
||||
assertThat(authentication.getAdditionalParameters())
|
||||
.containsAllEntriesOf(accessTokenResponse.getAdditionalParameters());
|
||||
}
|
||||
}
|
||||
|
||||
+191
-127
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,10 +18,6 @@ package org.springframework.security.oauth2.client.web;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
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.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
@@ -39,36 +35,44 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.oauth2.core.TestOAuth2AccessTokens.noScopes;
|
||||
import static org.springframework.security.oauth2.core.TestOAuth2RefreshTokens.refreshToken;
|
||||
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges.success;
|
||||
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests.request;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2AuthorizationCodeGrantFilter}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
@PowerMockIgnore("javax.security.*")
|
||||
@PrepareForTest({OAuth2AuthorizationRequest.class, OAuth2AuthorizationExchange.class, OAuth2AuthorizationCodeGrantFilter.class})
|
||||
@RunWith(PowerMockRunner.class)
|
||||
public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
private ClientRegistration registration1;
|
||||
private String principalName1 = "principal-1";
|
||||
@@ -132,8 +136,7 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
// NOTE: A valid Authorization Response contains either a 'code' or 'error' parameter.
|
||||
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
@@ -143,94 +146,142 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestNotFoundThenNotProcessed() throws Exception {
|
||||
String requestUri = "/path";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseUrlDoesNotMatchAuthorizationRequestRedirectUriThenNotProcessed() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
request.setRequestURI(requestUri + "-no-match");
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseValidThenAuthorizationRequestRemoved() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/path");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestRedirectUriDoesNotMatchThenNotProcessed() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri);
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
authorizationResponse.setRequestURI(requestUri + "-no-match");
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
// gh-7963
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestRedirectUriParametersMatchThenProcessed() throws Exception {
|
||||
// 1) redirect_uri with query parameters
|
||||
String requestUri = "/callback/client-1";
|
||||
Map<String, String> parameters = new LinkedHashMap<>();
|
||||
parameters.put("param1", "value1");
|
||||
parameters.put("param2", "value2");
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri, parameters);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
verifyZeroInteractions(filterChain);
|
||||
|
||||
// 2) redirect_uri with query parameters AND authorization response additional parameters
|
||||
Map<String, String> additionalParameters = new LinkedHashMap<>();
|
||||
additionalParameters.put("auth-param1", "value1");
|
||||
additionalParameters.put("auth-param2", "value2");
|
||||
response = new MockHttpServletResponse();
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
authorizationResponse = createAuthorizationResponse(authorizationRequest, additionalParameters);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
verifyZeroInteractions(filterChain);
|
||||
}
|
||||
|
||||
// gh-7963
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestRedirectUriParametersDoesNotMatchThenNotProcessed() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
Map<String, String> parameters = new LinkedHashMap<>();
|
||||
parameters.put("param1", "value1");
|
||||
parameters.put("param2", "value2");
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest(requestUri, parameters);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
// 1) Parameter value
|
||||
Map<String, String> parametersNotMatch = new LinkedHashMap<>(parameters);
|
||||
parametersNotMatch.put("param2", "value8");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(
|
||||
createAuthorizationRequest(requestUri, parametersNotMatch));
|
||||
authorizationResponse.setSession(authorizationRequest.getSession());
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
|
||||
// 2) Parameter order
|
||||
parametersNotMatch = new LinkedHashMap<>();
|
||||
parametersNotMatch.put("param2", "value2");
|
||||
parametersNotMatch.put("param1", "value1");
|
||||
authorizationResponse = createAuthorizationResponse(
|
||||
createAuthorizationRequest(requestUri, parametersNotMatch));
|
||||
authorizationResponse.setSession(authorizationRequest.getSession());
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
verify(filterChain, times(2)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
|
||||
// 3) Parameter missing
|
||||
parametersNotMatch = new LinkedHashMap<>(parameters);
|
||||
parametersNotMatch.remove("param2");
|
||||
authorizationResponse = createAuthorizationResponse(
|
||||
createAuthorizationRequest(requestUri, parametersNotMatch));
|
||||
authorizationResponse.setSession(authorizationRequest.getSession());
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
verify(filterChain, times(3)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestMatchThenAuthorizationRequestRemoved() throws Exception {
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(request)).isNull();
|
||||
assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(authorizationResponse)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationFailsThenHandleOAuth2AuthorizationException() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT);
|
||||
when(this.authenticationManager.authenticate(any(Authentication.class)))
|
||||
.thenThrow(new OAuth2AuthorizationException(error));
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1?error=invalid_grant");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSavedToService() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
public void doFilterWhenAuthorizationSucceedsThenAuthorizedClientSavedToService() throws Exception {
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
|
||||
this.registration1.getRegistrationId(), this.principalName1);
|
||||
@@ -242,40 +293,31 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessThenRedirected() throws Exception {
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
public void doFilterWhenAuthorizationSucceedsThenRedirected() throws Exception {
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessHasSavedRequestThenRedirectedToSavedRequest() throws Exception {
|
||||
public void doFilterWhenAuthorizationSucceedsAndHasSavedRequestThenRedirectToSavedRequest() throws Exception {
|
||||
String requestUri = "/saved-request";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
RequestCache requestCache = new HttpSessionRequestCache();
|
||||
requestCache.saveRequest(request, response);
|
||||
|
||||
requestUri = "/callback/client-1";
|
||||
request.setRequestURI(requestUri);
|
||||
request.setRequestURI("/callback/client-1");
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
@@ -285,36 +327,30 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception {
|
||||
public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception {
|
||||
AnonymousAuthenticationToken anonymousPrincipal =
|
||||
new AnonymousAuthenticationToken("key-1234", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
securityContext.setAuthentication(anonymousPrincipal);
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
|
||||
this.registration1.getRegistrationId(), anonymousPrincipal, request);
|
||||
this.registration1.getRegistrationId(), anonymousPrincipal, authorizationResponse);
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
|
||||
assertThat(authorizedClient.getPrincipalName()).isEqualTo(anonymousPrincipal.getName());
|
||||
assertThat(authorizedClient.getAccessToken()).isNotNull();
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
HttpSession session = authorizationResponse.getSession(false);
|
||||
assertThat(session).isNotNull();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -326,33 +362,27 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationResponseSuccessAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception {
|
||||
public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessNullAuthenticationThenAuthorizedClientSavedToHttpSession() throws Exception {
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
SecurityContextHolder.setContext(securityContext); // null Authentication
|
||||
|
||||
String requestUri = "/callback/client-1";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
request.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1");
|
||||
MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.setUpAuthorizationRequest(request, response, this.registration1);
|
||||
this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1);
|
||||
this.setUpAuthenticationResult(this.registration1);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
this.filter.doFilter(authorizationResponse, response, filterChain);
|
||||
|
||||
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
|
||||
this.registration1.getRegistrationId(), null, request);
|
||||
this.registration1.getRegistrationId(), null, authorizationResponse);
|
||||
assertThat(authorizedClient).isNotNull();
|
||||
|
||||
assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
|
||||
assertThat(authorizedClient.getPrincipalName()).isEqualTo("anonymousUser");
|
||||
assertThat(authorizedClient.getAccessToken()).isNotNull();
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
HttpSession session = authorizationResponse.getSession(false);
|
||||
assertThat(session).isNotNull();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -363,23 +393,57 @@ public class OAuth2AuthorizationCodeGrantFilterTests {
|
||||
assertThat(authorizedClients.values().iterator().next()).isSameAs(authorizedClient);
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest createAuthorizationRequest(String requestUri) {
|
||||
return createAuthorizationRequest(requestUri, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest createAuthorizationRequest(String requestUri, Map<String, String> parameters) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
if (!CollectionUtils.isEmpty(parameters)) {
|
||||
parameters.forEach(request::addParameter);
|
||||
request.setQueryString(
|
||||
parameters.entrySet().stream()
|
||||
.map(e -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining("&")));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest createAuthorizationResponse(MockHttpServletRequest authorizationRequest) {
|
||||
return createAuthorizationResponse(authorizationRequest, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest createAuthorizationResponse(
|
||||
MockHttpServletRequest authorizationRequest, Map<String, String> additionalParameters) {
|
||||
MockHttpServletRequest authorizationResponse = new MockHttpServletRequest(
|
||||
authorizationRequest.getMethod(), authorizationRequest.getRequestURI());
|
||||
authorizationResponse.setServletPath(authorizationRequest.getRequestURI());
|
||||
authorizationRequest.getParameterMap().forEach(authorizationResponse::addParameter);
|
||||
authorizationResponse.addParameter(OAuth2ParameterNames.CODE, "code");
|
||||
authorizationResponse.addParameter(OAuth2ParameterNames.STATE, "state");
|
||||
additionalParameters.forEach(authorizationResponse::addParameter);
|
||||
authorizationResponse.setQueryString(
|
||||
authorizationResponse.getParameterMap().entrySet().stream()
|
||||
.map(e -> e.getKey() + "=" + e.getValue()[0])
|
||||
.collect(Collectors.joining("&")));
|
||||
authorizationResponse.setSession(authorizationRequest.getSession());
|
||||
return authorizationResponse;
|
||||
}
|
||||
|
||||
private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response,
|
||||
ClientRegistration registration) {
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
|
||||
OAuth2AuthorizationRequest authorizationRequest = mock(OAuth2AuthorizationRequest.class);
|
||||
when(authorizationRequest.getAdditionalParameters()).thenReturn(additionalParameters);
|
||||
when(authorizationRequest.getRedirectUri()).thenReturn(request.getRequestURL().toString());
|
||||
when(authorizationRequest.getState()).thenReturn("state");
|
||||
OAuth2AuthorizationRequest authorizationRequest = request()
|
||||
.additionalParameters(additionalParameters)
|
||||
.redirectUri(UrlUtils.buildFullRequestUrl(request)).build();
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
|
||||
}
|
||||
|
||||
private void setUpAuthenticationResult(ClientRegistration registration) {
|
||||
OAuth2AuthorizationCodeAuthenticationToken authentication = mock(OAuth2AuthorizationCodeAuthenticationToken.class);
|
||||
when(authentication.getClientRegistration()).thenReturn(registration);
|
||||
when(authentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class));
|
||||
when(authentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class));
|
||||
when(authentication.getRefreshToken()).thenReturn(mock(OAuth2RefreshToken.class));
|
||||
OAuth2AuthorizationCodeAuthenticationToken authentication =
|
||||
new OAuth2AuthorizationCodeAuthenticationToken(registration, success(), noScopes(), refreshToken());
|
||||
when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication);
|
||||
}
|
||||
}
|
||||
|
||||
+154
-39
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,25 +25,28 @@ import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthorizationCodeAuthenticationTokens;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.server.handler.DefaultWebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests.request;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
@@ -101,7 +104,7 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
|
||||
MockServerWebExchange exchange = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("/"));
|
||||
DefaultWebFilterChain chain = new DefaultWebFilterChain(
|
||||
e -> e.getResponse().setComplete());
|
||||
e -> e.getResponse().setComplete(), Collections.emptyList());
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
|
||||
@@ -110,42 +113,154 @@ public class OAuth2AuthorizationCodeGrantWebFilterTests {
|
||||
|
||||
@Test
|
||||
public void filterWhenMatchThenAuthorizedClientSaved() {
|
||||
OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request()
|
||||
.redirectUri("/authorize/registration-id")
|
||||
.build();
|
||||
OAuth2AuthorizationResponse authorizationResponse = TestOAuth2AuthorizationResponses.success()
|
||||
.redirectUri("/authorize/registration-id")
|
||||
.build();
|
||||
OAuth2AuthorizationExchange authorizationExchange =
|
||||
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse);
|
||||
ClientRegistration registration = TestClientRegistrations.clientRegistration().build();
|
||||
Mono<Authentication> authentication = Mono.just(
|
||||
new OAuth2AuthorizationCodeAuthenticationToken(registration, authorizationExchange));
|
||||
OAuth2AuthorizationCodeAuthenticationToken authenticated = TestOAuth2AuthorizationCodeAuthenticationTokens
|
||||
.authenticated();
|
||||
|
||||
when(this.authenticationManager.authenticate(any())).thenReturn(
|
||||
Mono.just(authenticated));
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(any()))
|
||||
.thenReturn(Mono.just(clientRegistration));
|
||||
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
|
||||
.thenReturn(Mono.empty());
|
||||
ServerAuthenticationConverter converter = e -> authentication;
|
||||
when(this.authenticationManager.authenticate(any()))
|
||||
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
|
||||
|
||||
this.filter = new OAuth2AuthorizationCodeGrantWebFilter(
|
||||
this.authenticationManager, converter, this.authorizedClientRepository);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/authorize/registration-id")
|
||||
.queryParam(OAuth2ParameterNames.CODE, "code")
|
||||
.queryParam(OAuth2ParameterNames.STATE, "state")
|
||||
.build();
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
MockServerHttpRequest authorizationRequest =
|
||||
createAuthorizationRequest("/authorization/callback");
|
||||
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
|
||||
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
|
||||
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
|
||||
DefaultWebFilterChain chain = new DefaultWebFilterChain(
|
||||
e -> e.getResponse().setComplete());
|
||||
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, exchange).block();
|
||||
e -> e.getResponse().setComplete(), Collections.emptyList());
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
|
||||
verify(this.authorizedClientRepository).saveAuthorizedClient(any(), any(AnonymousAuthenticationToken.class), any());
|
||||
}
|
||||
|
||||
// gh-7966
|
||||
@Test
|
||||
public void filterWhenAuthorizationRequestRedirectUriParametersMatchThenProcessed() {
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
when(this.clientRegistrationRepository.findByRegistrationId(any()))
|
||||
.thenReturn(Mono.just(clientRegistration));
|
||||
when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any()))
|
||||
.thenReturn(Mono.empty());
|
||||
when(this.authenticationManager.authenticate(any()))
|
||||
.thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated()));
|
||||
|
||||
// 1) redirect_uri with query parameters
|
||||
Map<String, String> parameters = new LinkedHashMap<>();
|
||||
parameters.put("param1", "value1");
|
||||
parameters.put("param2", "value2");
|
||||
MockServerHttpRequest authorizationRequest =
|
||||
createAuthorizationRequest("/authorization/callback", parameters);
|
||||
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
|
||||
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
|
||||
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
|
||||
DefaultWebFilterChain chain = new DefaultWebFilterChain(
|
||||
e -> e.getResponse().setComplete(), Collections.emptyList());
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
verify(this.authenticationManager, times(1)).authenticate(any());
|
||||
|
||||
// 2) redirect_uri with query parameters AND authorization response additional parameters
|
||||
Map<String, String> additionalParameters = new LinkedHashMap<>();
|
||||
additionalParameters.put("auth-param1", "value1");
|
||||
additionalParameters.put("auth-param2", "value2");
|
||||
authorizationResponse = createAuthorizationResponse(authorizationRequest, additionalParameters);
|
||||
exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
verify(this.authenticationManager, times(2)).authenticate(any());
|
||||
}
|
||||
|
||||
// gh-7966
|
||||
@Test
|
||||
public void filterWhenAuthorizationRequestRedirectUriParametersNotMatchThenNotProcessed() {
|
||||
String requestUri = "/authorization/callback";
|
||||
Map<String, String> parameters = new LinkedHashMap<>();
|
||||
parameters.put("param1", "value1");
|
||||
parameters.put("param2", "value2");
|
||||
MockServerHttpRequest authorizationRequest =
|
||||
createAuthorizationRequest(requestUri, parameters);
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
OAuth2AuthorizationRequest oauth2AuthorizationRequest =
|
||||
createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration);
|
||||
|
||||
// 1) Parameter value
|
||||
Map<String, String> parametersNotMatch = new LinkedHashMap<>(parameters);
|
||||
parametersNotMatch.put("param2", "value8");
|
||||
MockServerHttpRequest authorizationResponse = createAuthorizationResponse(
|
||||
createAuthorizationRequest(requestUri, parametersNotMatch));
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
|
||||
DefaultWebFilterChain chain = new DefaultWebFilterChain(
|
||||
e -> e.getResponse().setComplete(), Collections.emptyList());
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
verifyZeroInteractions(this.authenticationManager);
|
||||
|
||||
// 2) Parameter order
|
||||
parametersNotMatch = new LinkedHashMap<>();
|
||||
parametersNotMatch.put("param2", "value2");
|
||||
parametersNotMatch.put("param1", "value1");
|
||||
authorizationResponse = createAuthorizationResponse(
|
||||
createAuthorizationRequest(requestUri, parametersNotMatch));
|
||||
exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
verifyZeroInteractions(this.authenticationManager);
|
||||
|
||||
// 3) Parameter missing
|
||||
parametersNotMatch = new LinkedHashMap<>(parameters);
|
||||
parametersNotMatch.remove("param2");
|
||||
authorizationResponse = createAuthorizationResponse(
|
||||
createAuthorizationRequest(requestUri, parametersNotMatch));
|
||||
exchange = MockServerWebExchange.from(authorizationResponse);
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(oauth2AuthorizationRequest, exchange).block();
|
||||
|
||||
this.filter.filter(exchange, chain).block();
|
||||
verifyZeroInteractions(this.authenticationManager);
|
||||
}
|
||||
|
||||
private static OAuth2AuthorizationRequest createOAuth2AuthorizationRequest(
|
||||
MockServerHttpRequest authorizationRequest, ClientRegistration registration) {
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
|
||||
return request()
|
||||
.additionalParameters(additionalParameters)
|
||||
.redirectUri(authorizationRequest.getURI().toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static MockServerHttpRequest createAuthorizationRequest(String requestUri) {
|
||||
return createAuthorizationRequest(requestUri, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
private static MockServerHttpRequest createAuthorizationRequest(String requestUri, Map<String, String> parameters) {
|
||||
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest
|
||||
.get(requestUri);
|
||||
if (!CollectionUtils.isEmpty(parameters)) {
|
||||
parameters.forEach(builder::queryParam);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static MockServerHttpRequest createAuthorizationResponse(MockServerHttpRequest authorizationRequest) {
|
||||
return createAuthorizationResponse(authorizationRequest, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
private static MockServerHttpRequest createAuthorizationResponse(
|
||||
MockServerHttpRequest authorizationRequest, Map<String, String> additionalParameters) {
|
||||
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest
|
||||
.get(authorizationRequest.getURI().toString());
|
||||
builder.queryParam(OAuth2ParameterNames.CODE, "code");
|
||||
builder.queryParam(OAuth2ParameterNames.STATE, "state");
|
||||
additionalParameters.forEach(builder::queryParam);
|
||||
builder.cookies(authorizationRequest.getCookies());
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
+11
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -57,8 +57,8 @@ import java.util.stream.Stream;
|
||||
public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2AccessTokenResponse> {
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, String>>() {};
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
|
||||
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
|
||||
|
||||
@@ -82,10 +82,16 @@ public class OAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpM
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
try {
|
||||
// gh-6463
|
||||
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> tokenResponseParameters = (Map<String, String>) this.jsonMessageConverter.read(
|
||||
Map<String, Object> tokenResponseParameters = (Map<String, Object>) this.jsonMessageConverter.read(
|
||||
PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage);
|
||||
return this.tokenResponseConverter.convert(tokenResponseParameters);
|
||||
return this.tokenResponseConverter.convert(
|
||||
tokenResponseParameters.entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> entry.getValue().toString())));
|
||||
} catch (Exception ex) {
|
||||
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Access Token Response: " +
|
||||
ex.getMessage(), ex, inputMessage);
|
||||
|
||||
+12
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -35,6 +35,7 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A {@link HttpMessageConverter} for an {@link OAuth2Error OAuth 2.0 Error}.
|
||||
@@ -47,8 +48,8 @@ import java.util.Map;
|
||||
public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2Error> {
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, String>> PARAMETERIZED_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, String>>() {};
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
|
||||
private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
|
||||
|
||||
@@ -70,10 +71,16 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
try {
|
||||
// gh-8157
|
||||
// Parse parameter values as Object in order to handle potential JSON Object and then convert values to String
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> errorParameters = (Map<String, String>) this.jsonMessageConverter.read(
|
||||
Map<String, Object> errorParameters = (Map<String, Object>) this.jsonMessageConverter.read(
|
||||
PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage);
|
||||
return this.errorConverter.convert(errorParameters);
|
||||
return this.errorConverter.convert(
|
||||
errorParameters.entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> String.valueOf(entry.getValue()))));
|
||||
} catch (Exception ex) {
|
||||
throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Error: " +
|
||||
ex.getMessage(), ex, inputMessage);
|
||||
|
||||
+34
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -96,6 +96,39 @@ public class OAuth2AccessTokenResponseHttpMessageConverterTests {
|
||||
|
||||
}
|
||||
|
||||
// gh-6463
|
||||
@Test
|
||||
public void readInternalWhenSuccessfulTokenResponseWithObjectThenReadOAuth2AccessTokenResponse() throws Exception {
|
||||
String tokenResponse = "{\n" +
|
||||
" \"access_token\": \"access-token-1234\",\n" +
|
||||
" \"token_type\": \"bearer\",\n" +
|
||||
" \"expires_in\": 3600,\n" +
|
||||
" \"scope\": \"read write\",\n" +
|
||||
" \"refresh_token\": \"refresh-token-1234\",\n" +
|
||||
" \"custom_object_1\": {\"name1\": \"value1\"},\n" +
|
||||
" \"custom_object_2\": [\"value1\", \"value2\"],\n" +
|
||||
" \"custom_parameter_1\": \"custom-value-1\",\n" +
|
||||
" \"custom_parameter_2\": \"custom-value-2\"\n" +
|
||||
"}\n";
|
||||
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(
|
||||
tokenResponse.getBytes(), HttpStatus.OK);
|
||||
|
||||
OAuth2AccessTokenResponse accessTokenResponse = this.messageConverter.readInternal(
|
||||
OAuth2AccessTokenResponse.class, response);
|
||||
|
||||
assertThat(accessTokenResponse.getAccessToken().getTokenValue()).isEqualTo("access-token-1234");
|
||||
assertThat(accessTokenResponse.getAccessToken().getTokenType()).isEqualTo(OAuth2AccessToken.TokenType.BEARER);
|
||||
assertThat(accessTokenResponse.getAccessToken().getExpiresAt()).isBeforeOrEqualTo(Instant.now().plusSeconds(3600));
|
||||
assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write");
|
||||
assertThat(accessTokenResponse.getRefreshToken().getTokenValue()).isEqualTo("refresh-token-1234");
|
||||
assertThat(accessTokenResponse.getAdditionalParameters()).containsExactly(
|
||||
entry("custom_object_1", "{name1=value1}"),
|
||||
entry("custom_object_2", "[value1, value2]"),
|
||||
entry("custom_parameter_1", "custom-value-1"),
|
||||
entry("custom_parameter_2", "custom-value-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
|
||||
Converter tokenResponseConverter = mock(Converter.class);
|
||||
|
||||
+20
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -78,6 +78,25 @@ public class OAuth2ErrorHttpMessageConverterTests {
|
||||
assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2");
|
||||
}
|
||||
|
||||
// gh-8157
|
||||
@Test
|
||||
public void readInternalWhenErrorResponseWithObjectThenReadOAuth2Error() throws Exception {
|
||||
String errorResponse = "{\n" +
|
||||
" \"error\": \"unauthorized_client\",\n" +
|
||||
" \"error_description\": \"The client is not authorized\",\n" +
|
||||
" \"error_codes\": [65001],\n" +
|
||||
" \"error_uri\": \"https://tools.ietf.org/html/rfc6749#section-5.2\"\n" +
|
||||
"}\n";
|
||||
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(
|
||||
errorResponse.getBytes(), HttpStatus.BAD_REQUEST);
|
||||
|
||||
OAuth2Error oauth2Error = this.messageConverter.readInternal(OAuth2Error.class, response);
|
||||
assertThat(oauth2Error.getErrorCode()).isEqualTo("unauthorized_client");
|
||||
assertThat(oauth2Error.getDescription()).isEqualTo("The client is not authorized");
|
||||
assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() {
|
||||
Converter errorConverter = mock(Converter.class);
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ class JettyCasService extends Server {
|
||||
String password = System.getProperty('javax.net.ssl.trustStorePassword','password')
|
||||
|
||||
|
||||
SslContextFactory sslContextFactory = new SslContextFactory();
|
||||
SslContextFactory sslContextFactory = new SslContextFactory.Server();
|
||||
sslContextFactory.setKeyStorePath(getTrustStore());
|
||||
sslContextFactory.setKeyStorePassword(password);
|
||||
sslContextFactory.setKeyManagerPassword(password);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
releasenotes:
|
||||
sections:
|
||||
- title: "New Features"
|
||||
emoji: ":star:"
|
||||
labels: ["enhancement"]
|
||||
- title: "Bug Fixes"
|
||||
emoji: ":beetle:"
|
||||
labels: ["bug", "regression"]
|
||||
- title: "Dependency Upgrades"
|
||||
emoji: ":hammer:"
|
||||
labels: ["dependency-upgrade"]
|
||||
- title: "Non-passive"
|
||||
emoji: ":rewind:"
|
||||
labels: ["breaks-passivity"]
|
||||
+1
-1
@@ -43,7 +43,7 @@ final class WithMockUserSecurityContextFactory implements
|
||||
.username() : withUser.value();
|
||||
if (username == null) {
|
||||
throw new IllegalArgumentException(withUser
|
||||
+ " cannot have null username on both username and value properites");
|
||||
+ " cannot have null username on both username and value properties");
|
||||
}
|
||||
|
||||
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
|
||||
|
||||
+1
-1
@@ -563,6 +563,6 @@ public class SwitchUserFilter extends GenericFilterBean
|
||||
}
|
||||
|
||||
private static RequestMatcher createMatcher(String pattern) {
|
||||
return new AntPathRequestMatcher(pattern, null, true, new UrlPathHelper());
|
||||
return new AntPathRequestMatcher(pattern, "POST", true, new UrlPathHelper());
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -42,7 +42,6 @@ import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -82,7 +81,7 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
|
||||
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
|
||||
private AuthenticationEntryPoint authenticationEntryPoint;
|
||||
private AuthenticationManager authenticationManager;
|
||||
private LogoutHandler logoutHandler;
|
||||
private List<LogoutHandler> logoutHandlers;
|
||||
|
||||
HttpServlet3RequestFactory(String rolePrefix) {
|
||||
this.rolePrefix = rolePrefix;
|
||||
@@ -146,7 +145,7 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
|
||||
* {@link HttpServletRequest#logout()}.
|
||||
*/
|
||||
public void setLogoutHandlers(List<LogoutHandler> logoutHandlers) {
|
||||
this.logoutHandler = CollectionUtils.isEmpty(logoutHandlers) ? null : new CompositeLogoutHandler(logoutHandlers);
|
||||
this.logoutHandlers = logoutHandlers;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,8 +245,8 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
|
||||
|
||||
@Override
|
||||
public void logout() throws ServletException {
|
||||
LogoutHandler handler = HttpServlet3RequestFactory.this.logoutHandler;
|
||||
if (handler == null) {
|
||||
List<LogoutHandler> handlers = HttpServlet3RequestFactory.this.logoutHandlers;
|
||||
if (CollectionUtils.isEmpty(handlers)) {
|
||||
HttpServlet3RequestFactory.this.logger.debug(
|
||||
"logoutHandlers is null, so allowing original HttpServletRequest to handle logout");
|
||||
super.logout();
|
||||
@@ -255,7 +254,9 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
|
||||
}
|
||||
Authentication authentication = SecurityContextHolder.getContext()
|
||||
.getAuthentication();
|
||||
handler.logout(this, this.response, authentication);
|
||||
for (LogoutHandler handler : handlers) {
|
||||
handler.logout(this, this.response, authentication);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAuthenticated() {
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ public final class AntPathRequestMatcher
|
||||
*
|
||||
* @param pattern the ant pattern to use for matching
|
||||
* @param httpMethod the HTTP method. The {@code matches} method will return false if
|
||||
* the incoming request doesn't doesn't have the same method.
|
||||
* the incoming request doesn't have the same method.
|
||||
* @param caseSensitive true if the matcher should consider case, else false
|
||||
* @param urlPathHelper if non-null, will be used for extracting the path from the HttpServletRequest
|
||||
*/
|
||||
|
||||
+42
-5
@@ -16,11 +16,17 @@
|
||||
|
||||
package org.springframework.security.web.authentication.switchuser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import javax.servlet.FilterChain;
|
||||
|
||||
import org.junit.*;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.AccountExpiredException;
|
||||
@@ -42,8 +48,10 @@ import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import java.util.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests
|
||||
@@ -75,6 +83,7 @@ public class SwitchUserFilterTests {
|
||||
request.setScheme("http");
|
||||
request.setServerName("localhost");
|
||||
request.setRequestURI("/login/impersonate");
|
||||
request.setMethod("POST");
|
||||
|
||||
return request;
|
||||
}
|
||||
@@ -125,6 +134,20 @@ public class SwitchUserFilterTests {
|
||||
assertThat(filter.requiresExitUser(request)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
// gh-4183
|
||||
public void requiresExitUserWhenGetThenDoesNotMatch() {
|
||||
SwitchUserFilter filter = new SwitchUserFilter();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setScheme("http");
|
||||
request.setServerName("localhost");
|
||||
request.setRequestURI("/login/impersonate");
|
||||
request.setMethod("GET");
|
||||
|
||||
assertThat(filter.requiresExitUser(request)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requiresExitUserWhenMatcherThenWorks() {
|
||||
SwitchUserFilter filter = new SwitchUserFilter();
|
||||
@@ -159,6 +182,20 @@ public class SwitchUserFilterTests {
|
||||
assertThat(filter.requiresSwitchUser(request)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
// gh-4183
|
||||
public void requiresSwitchUserWhenGetThenDoesNotMatch() {
|
||||
SwitchUserFilter filter = new SwitchUserFilter();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setScheme("http");
|
||||
request.setServerName("localhost");
|
||||
request.setRequestURI("/login/impersonate");
|
||||
request.setMethod("GET");
|
||||
|
||||
assertThat(filter.requiresSwitchUser(request)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requiresSwitchUserWhenMatcherThenWorks() {
|
||||
SwitchUserFilter filter = new SwitchUserFilter();
|
||||
|
||||
Reference in New Issue
Block a user